python中的可变、不可变对象

好久没来写blog,期间一直在忙论文和实习的事情。准备等适应了,好好写一些深度学习框架和实现的文章,作为研二收尾的成果。

今天的motivation来自写python当中的一个问题,简化如下:(当我在做数据分析的时候,将数据存入列表或者字典中,传入函数后原来列表发生改变与否,关键字:in-place,原地操作)

def fun():
    data = ['我在fun()里,我是原始数据,我不想改变']
    print(data)
    data2 = fun2(data)
    return data,data2

def fun2(data):
    data.append('我在fun2(),原始数据被fun2处理了')
    return data  
result1,result2 = fun()
print('data:',result1,'data2:',result2)

这里将data作为一个列表参数,传参进函数fun2后,自身发生了改变。也就是发生了类似于C语言的引用传递,但是否所有数据类型都是这样?如果把data改为int,其实这里变化又会不一样。经过几天的搜寻答案,牵扯到的知识越来越多(局部变量,可变、不可变对象,浅复制、深复制),这里先总结一波。

一,python中树立最主要的概念是可变(不可变)对象。它是其他知识的铺垫。

python不可变对象:int,string,float,tuple
python可变对象:dict,list

主要在作为参数传入函数时,
因为,python的函数参数传递,既不是值传递,也不是引用传递。它的传递方式是”传对象“。函数参数在传递的过程中,将整个对象传入。(《编写高质量的代码——改成python程序的n个建议》)
1.对可变对象的修改在函数外部以及内部都可见;
2.对于不可变对象,由于不能真正的修改,往往是创建一个新的对象,然后通过赋值来实现。所以,外部是不可见的。

具体代码测试参见文章1及文章2

重新分析上述代码:

def fun():
    data = ['我在fun()里,我是原始数据,我不想改变']  #data为list类型,可变对象
    print(data)
    data2 = fun2(data)
    return data,data2

def fun2(data):
    data.append('我在fun2(),原始数据被fun2处理了')
    return data  
result1,result2 = fun()
print('data:',result1,'data2:',result2)

二,所谓的“全局变量”和“局部变量”。

在python中,“一切皆为对象”,所以变量的概念其实也是一种变量,诸如上述的data,其实是字符串对象,与列表对象进行映射。
由于这是一种映射关系,所以,可以使用键-值的形式来表示,即{name : object}。这里先引出命名空间的概念。

命名空间是对变量名的分组划分。
不同组的相同名称的变量视为两个独立的变量,因此隶属于不同分组(即命名空间)的变量名可以重复。
命名空间可以存在多个,使用命名空间,表示在该命名空间中查找当前名称。

前面已经说过,命名空间是对变量名的分组划分,所以,Python的命名空间就是对许多键-值对的分组划分,即,键值对的集合,因此:
Python的命名空间是一个字典,字典内保存了变量名称与对象之间的映射关系。如何查找字典内的映射关系?这根语言的机制有关,Python的查找规则为LEGB。
LEGB规定了查找一个名称的顺序为:local–>enclosing function locals–>global–>builtin
LEGB细节参见https://www.jianshu.com/p/3b72ba5a209c

这样的问题是为了应对如下代码:
代码1:

def createCounter():
    s = [0]   # s是列表对象,它是可变对象,可以设它的地址为d1;其中的元素0地址设为d2
    def counter():
        s[0]+=1   #s[0]指向int对象,他是不可变对象,所以由地址由d2月更改了,设为d3;s列表地址仍然为d1
        return s[0]
    return counter

代码2:

def createCounter():
    i=0
    def counter():
        nonlocal  i
        i= i+1
        return i
    return counter

注意:修改全局变量或者取消局部变量,可以使用global和nonlocal关键字。一般说来函数内部不能改变外部的量,指的是对象的内存地址不能改变。
1)如果是可变变量,如代码1中的s列表对象,修改其中的元素s[0],s[0]因为是int(不可变对象),所以s[0]的内存地址发生改变,但不影响s列表的内存地址。所以代码1中无需对s加nonlocal关键字;
2)如果不可变变量,如代码2中的i,其为int,修改时(i=i+1),首先要对i进行引用并且必须改变内存地址,所以如果不加关键字nonlocal,则会报local variable ‘i’ referenced before assignment的错误。但是,如果此时将i=i+1换成 i=5,则没有nonlocal也不会报错,因为此时的i为针对counter函数的局部变量,仅仅和外部函数体内的i重名而已。
这里的https://www.cnblogs.com/Archmage/p/7569817.html 和https://www.cnblogs.com/yanfengt/p/6305542.html可以作为参考。
结论:引用全局变量,不需要golbal声明,修改全局变量,需要使用global声明,特别地,列表、字典等如果只是修改其中元素的值,可以直接使用全局变量,不需要global声明。

def fun():
    data = ['我在fun()里,我是原始数据,我不想改变']  #data为list类型,可变对象
    print(data)
#==============================================================================
# 	def fun2():
# 	   data.append('我在fun2(),原始数据被fun2处理了')
# 	   return data  
#==============================================================================
    data2 = fun2()
    return data,data2

def fun2():
    data.append('我在fun2(),原始数据被fun2处理了')
    return data  
result1,result2 = fun()
print('data:',result1,'data2:',result2)

当fun2取消参数传入时,
1)如果引用外部函数,则会报data没有define的错误,此时必须要传入参数。
2)当引用内部函数(使用闭包,也就是将fun2定义在fun函数体内),fun2函数体local作用域里已经没有data这个对象,所以data进行append操作时对象为fun函数体内的data(注意这时候没有报错,是因为按照LEGB机制,这时候到了E作用域内,也就是fun2外,fun内的data对象)

在上述例子中,从内到外,依次形成四个命名空间:
def fun2():Local, 即函数内部命名空间;
def fun():Enclosing function locals;外部嵌套函数的名字空间
module(文件本身):Global(module);函数定义所在模块(文件)的名字空间
Python内置模块的名字空间:Builtin

三,通过一和二的知识,我们来分析以下代码:
代码1:

def test_func(a):
    a[0] = 'a'
    a = ['b', 'c', 'd']
    return a

a = ['c', 'd', 'e']
test_func(a)
print(a)
#a为['a' ,'d', 'e']

代码2:

def test_func():
    global a
    a = ['b', 'c', 'd']
    a[0] = 'a'

a = ['c', 'd', 'e']
test_func()
print(a)
#a为['a', 'c', 'd']

代码3:

def test_func():
    a = ['b', 'c', 'd']
    a[0] = 'a'

a = ['c', 'd', 'e']
test_func()
print(a)
#a为['c', 'd', 'e']

三、关于赋值、浅复制和深复制

引用博客园的这篇文章的结论:

• Python中对象的赋值都是进行对象引用(内存地址)传递
• 使用copy.copy(),可以进行对象的浅拷贝,它复制了对象,但对于对象中的元素,依然使用原始的引用.
• 如果需要复制一个容器对象,以及它里面的所有元素(包含元素的子元素),可以使用copy.deepcopy()进行深拷贝
• 对于非容器类型(如数字、字符串、和其他'原子'类型的对象)没有被拷贝一说

重点说的是切片[:]这种方式,在list中切片相当于浅拷贝,这时候是创建了新的对象,但是对象中的元素,依然使用原始的引用;而在numpy中,切片方式则是做in-place的操作。在numpy库对象使用中成立,numpy库的array对象切片操作是会对本身进行操作,因为一般都是处理大型矩阵,这样大大的节省了开销,同时也提高了效率。

总结

做数据分析的时候,如果传参的时候,想保留原数据:

  1. 如果原数据是不可变对象,则不会做in-place操作,没有这种顾虑。
  2. 如果原数据是可变对象,如果是列表,则可以用切片[:]或者copy.copy()来进行浅拷贝,但仍然有隐患。如果列表中有可变元素(列表中有列表),修改此元素仍然会改变原数据。最稳妥的方式是使用copy.deepcopy()进行深拷贝
  3. numpy中ndarray的切片[:]是in-place操作

你可能感兴趣的:(杂说)