python中的赋值语句
python中关于对象复制有三种类型的使用方式,赋值、浅拷贝与深拷贝。在
python 中赋值语句总是建立对象的引用值,而不是复制对象。因此,python 变量更像是指针,而不是数据存储区域。如下图所示:
>>> list_a = [1,2,3,"hello",[4,5]]
>>> list_b = list_a
>>> list_b
[1, 2, 3, 'hello', [4, 5]]
>>> list_b is list_a
True
>>> id(list_a)
30787784
>>> id(list_b)
30787784如上面代码,把list_a赋值给list_b,意味着两个指向同一个内存空间,通过查看两个list值和id发现两者相同。赋值操作(包括对象作为参数、返回值)不会开辟新的内存空间,它只是复制了新对象的引用。也就是说,除了list_b这个名字以外,没有其它的内存开销。修改了list_a,就影响了list_b;同理,修改了list_b就影响了list_a。
简单偏题下关于id的一个问题:
>>> a = 2.5
>>> b = 2.5
>>> id(a)
19603848
>>> id(b)
30973856
>>> a is b
False
>>> a = 2
>>> b = 2
>>> a is b
True
>>> id(a)
19571404
>>> id(b)
19571404可以得到一个简单的结论就是:解释器在对值很小的int和很短的字符串的时候做了一点小优化,只分配了一个对象,让它们id一样了。
>>> values = [1,2,3]
>>> values[1] = values
>>> values
[1, [...], 3]把list中第二个元素赋值为本身,本以为结果是[1,[1,2,3],3] ,发现输出结果是循环无限次,这里需要明确的一个概念是:Python 没有赋值,只有引用。你这样相当于创建了一个引用自身的结构,所以导致了无限循环。>>> values = [0,1,2]
>>> id(values)
19149888
>>> values = [3,4,5]
>>> id(values)
30779992
Python 做的事情是首先创建一个列表对象 [0, 1, 2],然后给它贴上名为 values 的标签。如果随后又执行Python
做的事情是创建另一个列表对象 [3, 4, 5],然后把刚才那张名为 values 的标签从前面的 [0, 1, 2] 对象上撕下来,重新贴到 [3, 4, 5] 这个对象上。至始至终,并没有一个叫做
values 的列表对象容器存在,Python 也没有把任何对象的值复制进 values 去。过程如图所示:
当执行values[1]=values,Python 做的事情则是把 values 这个标签所引用的列表对象的第二个元素指向 values 所引用的列表对象本身。如下图:
从上图可以发现当要打印values
就会陷入死循环。
那么假如我需要使得values[1]
值就为[0,1,2] ,这时就需要涉及到数据拷贝问题。
python
深浅拷贝
浅拷贝
浅拷贝有三种形式:切片操作,工厂函数,copy模块中的copy函数
>>> values = [0,1,2]
>>> id(values)
30779592
>>> values[1] = values[:]
>>> id(values)
30779592
上述代码能使得values[1] 值就为[0,1,2],Python
做的事情是,先 dereference 得到 values 所指向的对象 [0, 1, 2],然后执行 [0, 1, 2][:] 复制操作得到一个新的对象,内容也是 [0, 1, 2],然后将 values 所指向的列表对象的第二个元素指向这个复制二来的列表对象,最终 values 指向的对象是 [0, [0, 1, 2], 2]。
>>> values = [0,1,2]
>>> id(values)
30779352
>>> id(values[1])
19571416
>>> values[1] = values[:]
>>> id(values[1])
30779712
>>> values
[0, [0, 1, 2], 2]
改变values[1] 后其id值发生改变过程如下图:
除了上面切片方法,还可以使用工厂函数方法:
>>> values = [0,1,2]
>>> id(values)
30779792
>>> id(values[1])
19571416
>>> values[1] = list(values)
>>> values
[0, [0, 1, 2], 2]
>>> id(values)
30779792
>>> id(values[1])
30440224
第三种方法使用copy模块的copy函数:
>>> import copy
>>> values = [0,1,2]
>>> id(values)
30779352
>>> id(values[1])
19571416
>>> values[1] = copy.copy(values)
>>> values
[0, [0, 1, 2], 2]
>>> id(values)
30779352
>>> id(values[1])
30774976
前面说了三种浅拷贝的方式,但是当list中出现嵌套结构时,会发生出乎意料的错误;
>>> a = [0,[1,2],3]
>>> b = a[:]
>>> id(a)
30777552
>>> id(b)
30780192
>>> id(a[1])
24241536
>>> id(b[1])
24241536
使用前面所说的切片浅拷贝,我们发现a,b 的id地址不同,但是a[1] b[1] 地址相同,如下图所示:
所以当我们对a[1]
或者b[1] 其中一个进行修改时,两个结构都会改变,可能我们不需要这种情况发生。
深拷贝
深拷贝在python中只有一种方法,copy模块中的deepcopy函数,deepcopy
本质上是递归 copy。
和浅拷贝对应,深拷贝拷贝了对象的所有元素,包括多层嵌套的元素。因而,它的时间和空间开销要高。
>>> a = [0,[1,2],3]
>>> b = copy.deepcopy(a)
>>> id(a)
30779992
>>> id(b)
30779792
>>> id(a[1])
30774376
>>> id(b[1])
30780472
当进行深拷贝我们发现,a[1] b[1] 地址空间不相同了,如下图所示:
所以这时我们对a 的任何修改将不会影响到 b。
对于不可变对象和可变对象来说,浅复制都是复制的引用,只是因为复制不变对象和复制不变对象的引用是等效的(因为对象不可变,当改变时会新建对象重新赋值)。所以看起来浅复制只复制不可变对象(整数,实数,字符串等),对于可变对象,浅复制其实是创建了一个对于该对象的引用,也就是说只是给同一个对象贴上了另一个标签而已。
L = [1, 2, 3]
D = {'a':1, 'b':2}
A = L[:]
B = D.copy()
print "L, D"
print L, D
print "A, B"
print A, B
print "--------------------"
A[1] = 'NI'
B['c'] = 'spam'
print "L, D"
print L, D
print "A, B"
print A, B
L, D
[1, 2, 3] {'a': 1, 'b': 2}
A, B
[1, 2, 3] {'a': 1, 'b': 2}
--------------------
L, D
[1, 2, 3] {'a': 1, 'b': 2}
A, B
[1, 'NI', 3] {'a': 1, 'c': 'spam', 'b': 2}
增强赋值与共享引用
x = x + y,x 出现两次,必须执行两次,性能不好,合并必须新建对象 x,然后复制两个列表合并属于复制/拷贝
x += y,x 只出现一次,也只会计算一次,性能好,不生成新对象,只在内存块末尾增加元素。
当 x、y 为list时, += 会自动调用 extend 方法进行合并运算,in-place change。属于共享引用
>>> L = [1,2]
>>> M = L
>>> id(M)
30774616
>>> id(L)
30774616
>>> L = L + [3,4]
>>> id(L)
24241336
>>> print L,M
[1, 2, 3, 4] [1, 2]
L = L + [3,4]这时会重新把L 指向 L + [3,4] 产生新对象地址,所以L id值发生改变
>>> L = [1,2]
>>> id(L)
30440224
>>> M = L
>>> id(M)
30440224
>>> L += [3,4]
>>> id(L)
30440224
>>> id(M)
30440224
>>> print L,M
[1, 2, 3, 4] [1, 2, 3, 4]
这里调用 +=不会产生新的临时对象更加高效。
深入理解python 变量作用域及陷阱
可变对象与不可变对象
在Python中,对象分为两种:可变对象和不可变对象,不可变对象包括int,float,long,str,tuple等,可变对象包括list,set,dict等。需要注意的是:这里说的不可变指的是值的不可变。对于不可变类型的变量,如果要更改变量,则会创建一个新值,把变量绑定到新值上,而旧值如果没有被引用就等待垃圾回收。另外,不可变的类型可以计算hash值,作为字典的key。可变类型数据对对象操作的时候,不需要再在其他地方申请内存,只需要在此对象后面连续申请(+/-)即可,也就是它的内存地址会保持不变,但区域会变长或者变短。
>>> str = "python"
>>> id(str)
19392416
>>> str = "java"
>>> id(str)
19393696
str类型不可改变,当str重新赋值后地址发生改变。
>>> list_a = [1,2,3]
>>> id(list_a)
30780352
>>> list_a.append(4)
>>> id(list_a)
30780352
list后面增加数据但是list 地址没有改变。
函数传值
>>> def func_int(a):
print id(a)
a += 4
print id(a)
>>> t = 0
>>> func_int(t)
19571428
19571380
>>> t
0
>>> id(t)
19571428
因为int 型数据不可该表,当在函数中对a+=4 操作a 指向了另外值为4的地址。
>>> def func_list(list_type):
print id(list_type)
list_type[0] = 4
print id(list_type)
>>> list_type = [1,2,3]
>>> func_list(list_type)
19149888
19149888
>>> list_type
[4, 2, 3]
>>> id(list_type)
19149888
对于上面的输出,不少Python初学者都比较疑惑:第一个例子看起来像是传值,而第二个例子确实传引用。其实,解释这个问题也非常容易,主要是因为可变对象和不可变对象的原因:对于可变对象,对象的操作不会重建对象,而对于不可变对象,每一次操作就重建新的对象。
在函数参数传递的时候,Python其实就是把参数里传入的变量对应的对象的引用依次赋值给对应的函数内部变量。参照上面的例子来说明更容易理解,func_int中的局部变量"a"其实是全部变量"t"所指向对象的另一个引用,由于整数对象是不可变的,所以当func_int对变量"a"进行修改的时候,实际上是将局部变量"a"指向到了整数对象"4"。所以很明显,func_list修改的是一个可变的对象,局部变量"a"和全局变量"list_type"指向的还是同一个对象。