好久没来写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不可变对象: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对象切片操作是会对本身进行操作,因为一般都是处理大型矩阵,这样大大的节省了开销,同时也提高了效率。
做数据分析的时候,如果传参的时候,想保留原数据: