赋值,浅拷贝,深拷贝不同!
非常值得参考:https://www.cnblogs.com/shenbuer/p/7977323.html
1.赋值(引用)和复制
Python 没有赋值,只有引用。Python 没有「变量」,我们平时所说的变量其实只是「标签」,是引用。
values=[0,1,2]
values[1]=values
print(values) # [0, [...], 2]
预想应该是:[0,[0,1,2],2],但结果却要赋值无限次。
Python 里面复制对象的操作因对象类型而异,复制列表 values 的操作是:
values[:] # 生成对象的拷贝或者是复制序列,不再是引用和共享变量,但此法只能顶层复制(所以叫做浅复制)
从而上述改为:
values[1]=values[:] # 输出结果就是
[0,[0,1,2],2]
Python 做的事情是,先 dereference (取消引用)得到 values 所指向的对象 [0, 1, 2],然后执行 [0, 1, 2][:] 复制操作得到一个新的对象,内容也是 [0, 1, 2],然后将 values 所指向的列表对象的第二个元素指向这个复制二来的列表对象,最终 values 指向的对象是 [0, [0, 1, 2], 2]
因为values[:] 复制操作是所谓的「浅复制」(shallow copy),当列表对象有嵌套的时候也会产生出乎意料的错误:
# 浅复制只能进行顶层复制,对于深层的采用的是引用而不是复制
a=[0,[1,2],3]
b=a[:]
a[0]=8
a[1][1]=9
print(a) # [8, [1, 9], 3]
print(b) # [0, [1, 9], 3] 可以看到里面的列表是变化的,因为是浅复制
a=[[0]*2]*2
b=[[0]*2 for _ in range(2)]
a[1][0]=3
b[1][0]=3
print(a) # [[3, 0], [3, 0]] 浅拷贝,所以深层的是引用
print(b) # [[0, 0], [3, 0]]
「浅复制」只能进行顶层复制,对于对象的内部的子对象(深层部分)是引用而不是复制,正确的复制嵌套元素的方法是进行「深复制」(deep copy),方法是:加载copy模块,使用copy.deepcopy()函数
import copy
a = [0, [1, 2], 3]
b = copy.deepcopy(a)
a[0] = 8
a[1][1] = 9
print(a) # a=[8,[1,9],3]
print(b) # b=[0,[1,2],3]和原始的a一样
2.引用和复制(包含浅复制和拷贝(深复制)):
(1) 没有限制条件的分片表达式(L[:])能够复制序列,但此法只能浅层复制
(2) 字典 copy 方法,D.copy() 能够复制字典,但此法只能浅层复制
(3) 有些内置函数,例如 list,能够生成拷贝 list(L)
(4) copy 标准库模块能够生成完整拷贝:copy.deepcopy 本质上是递归 copy
(5) copy标准模块的copy.copy()函数实现的是浅拷贝(浅复制),这里的功能比上面切片的复制范围更广,浅复制包括所有的对象。
(6) 对于不可变对象和可变对象来说,浅复制都是复制的引用,只是因为复制不变对象和复制不变对象的引用是等效的(因为对象不可变,当改变时会新建对象重新赋值)。所以看起来浅复制只复制不可变对象(整数,实数,字符串等),对于可变对象,浅复制其实是创建了一个对于该对象的引用,也就是说只是给同一个对象贴上了另一个标签而已。
(7)
import copy
a=[1,2,[3,4],{'a':1}] # 原始对象
print('原始a', a)
b=a # 赋值,传对象的引用,引用不同于拷贝
c=copy.copy(a) # 对象拷贝,浅拷贝
d=copy.deepcopy(a) # 对象拷贝,深拷贝
# e, f, g三个都是完全相同,不同的写法。
e=a[:] # 能复制序列,浅拷贝
f=[0]*len(a)
for i in range(len(a)): # 浅拷贝,和e一模一样
f[i]=a[i]
g = a[0:len(a)] # 浅拷贝,和e一模一样
a.append('add1') # 修改对象a
a[2].append('add2') # 修改对象a中的[3,4]数组对象
a[3]='666'
print('a:',a)
print('b:',b)
print('c:',c)
print('d:',d)
print('e:',e)
print('f:',f)
print('g:',g)
执行结果:
"""
执行结果:
原始a [1, 2, [3, 4], {'a': 1}]
原始f: [1, 2, [3, 4], {'a': 1}]
原始的g: [1, 2, [3, 4], {'a': 1}]
a: [1, 2, [3, 4, 'add2'], '666', 'add1']
b: [1, 2, [3, 4, 'add2'], '666', 'add1']
c: [1, 2, [3, 4, 'add2'], {'a': 1}]
d: [1, 2, [3, 4], {'a': 1}]
e: [1, 2, [3, 4, 'add2'], {'a': 1}]
f: [1, 2, [3, 4, 'add2'], {'a': 1}]
g: [1, 2, [3, 4, 'add2'], {'a': 1}]
"""
三、深入理解python变量作用域及其陷阱
1. 1 可变对象&不可变对象
在Python中,对象分为两种:可变对象和不可变对象,不可变对象包括int,float,long,str,tuple等,可变对象包括list,set,dict等。需要注意的是:这里说的不可变指的是值的不可变。对于不可变类型的变量,如果要更改变量,则会创建一个新值,把变量绑定到新值上,而旧值如果没有被引用就等待垃圾回收。另外,不可变的类型可以计算hash值,作为字典的key。可变类型数据对对象操作的时候,不需要再在其他地方申请内存,只需要在此对象后面连续申请(+/-)即可,也就是它的内存地址会保持不变,但区域会变长或者变短。
a='hello'
print(id(a)) # 1991608735200
a='python'
print(id(a)) # 1991608735368
""" 重新赋值之后,变量a的内存地址已经变了
'hello'是str类型,不可变,所以赋值操作知识重新创建了str 'python'对象,然后将变量a指向了它"""
l1=[1,2,3]
print(id(l1)) # 2262493958280
l1.append(4)
print(id(l1)) # 2262493958280
""" list重新赋值之后,变量l1的内存地址并未改变
[1, 2, 3]是可变的,append操作只是改变了其value,变量l1指向没有变"""
p=9
q=p
print(id(p),id(q)) # 输出 id(p)=140242625469728 id(q)=140242625469728,两者为同一个地址,因为赋值操作是引用
p=10
print('p:',p,'q:',q)
print('id(p):',id(p),'id(q)',id(q)) # 输出id(p)=140242625469760 id(q)=140242625469728,两者不一样
# 因为不可变对象的每一次操作都会重建新的对象
1.2 函数值传递
def func_int(a):
a+=4
def func_list(l1):
l1[0]=4
t=0
func_int(t)
print(t) # 输出不是4而是0
t_list=[1,2,3]
func_list(t_list)
print(t_list) # [4, 2, 3]符合我们的预想
主要是可变对象和不可变对象的原因:对于可变对象,对象的操作不会重建对象,而对于不可变对象,每一次操作就重建新的对象。在函数参数传递的时候,Python其实就是把参数里传入的变量对应的对象的引用依次赋值给对应的函数内部变量,举例:也就是对于func_int函数的a是t的赋值(引用操作)共同指向不可变对象0,后面对变量"a"进行修改的时候,实际上是将局部变量"a"指向到了整数对象"4";所以很明显,func_list修改的是一个可变的对象,局部变量"a"和全局变量"t_list"指向的还是同一个对象。
1.3 陷阱:使用可变的默认参数
def foo(a, b, c=[]):
c.append(a)
c.append(b)
print(id(c))
print(c)
foo(1, 1) # id=140242429217032 输出 [1, 1] 从id可以看到这三个对象是同一个对象,所以进行累加了
foo(2, 2) # id=140242429217032 输出 [1, 1, 2, 2] 不是我们想要的,却累加了
foo(3, 3) # id=140242429217032 输出 [1, 1, 2, 2, 3, 3] 不是我们想要的,却累加了
def fo(a, b, c=None):
if c is None:
c=[]
c.append(a)
c.append(b)
print(id(c))
print(c)
fo(1,1) # id=140242429650440 输出 [1, 1] 从id可以看到这些对象是不同对象
fo(2,2) # id=140242429239752 输出 [2, 2] 当指向这个对象的时候,不用的对象被垃圾回收释放掉了
fo(3,3) # id=140242429650440 输出 [3, 3] 被释放的内存可以看做一个新的对象
fo(5,6) # id=140242429239752 输出 [5, 6]