2024年08月10日
先来看个问题,python中交换两个变量值的语句:a,b=b,a
a= 1
b = 2
a,b = b,a
print('a = ' + str(a) + '\n' + 'b = ' + str(b))
结果:
a = 2
b = 1
交换两个变量的值,相信Python的这条指令颠覆很多人的三观。这是因为大家的三观是学C语言建立的。
在C语言中,系统会为每个变量分配内存空间。而在python中,Python为每个值分配内存空间。
进一步分析:
Python中变量只是一个标签,系统会为值分配内存空间,所以上面语句是先将“b值(2)”的地址赋给a变量,然后将“a值(1)”的地址赋给b变量,这样就完成了数据的交换。
然后可能会有人疑问,如果按照上面的解释,先将上面b值地址赋给a的话,那a不就是引用到2,后面再将a值地址赋给b的话,这样b不还是2吗?
因为赋值号是从右往左的赋值,所以会先将右边的表达式算出来值,而用逗号连接起来的表达式会从左往右依次执行,所以上面在进行赋值的时候Python内部会处理成 a,b = 2,1 这样就完成了数据的交换。
实际上,在大多数应用中,python引用所得到的结果就是你想要的,不过,因为赋值操作会产生相同对象的多个引用,需要意识到在原处修改可变对象时可能会影响程序中其他地方对相同对象的其他引用。如果你不想这样做,就需要明确地告诉Python复制该对象。
例如,下面这个例子生成一个列表并赋值为X,另一个列表赋值为L,L嵌套对列表X的引用。这一例子中还生成了一个字典D,含有另一个对列表X的引用。
x=[1,2,3,[4,4,4],5,6]
l=[1,x,2,3]
d={1:x,2:2}
print(x,'\n',l,'\n',d)
x[1]=0
print(x,'\n',l,'\n',d)
运行结果
[1, 2, 3, [4, 4, 4], 5, 6]
[1, [1, 2, 3, [4, 4, 4], 5, 6], 2, 3] {1: [1, 2, 3, [4, 4, 4], 5, 6], 2: 2} [1, 0, 3, [4, 4, 4], 5, 6]
[1, [1, 0, 3, [4, 4, 4], 5, 6], 2, 3] {1: [1, 0, 3, [4, 4, 4], 5, 6], 2: 2} [Program finished]
由于列表是可变的,修改这三个引用中任意一个共享列表对象,也会改变另外两个引用的对象。
引用是其他语言中指针的更高级模拟。虽然你不能抓住引用本身,但在不止一个地方储存相同的引用(变量、列表等)是可能的。这是一大特点:你可以在程序范围内任何地方传递大型对象而不必在途中产生拷贝。然而,如果你的确需要拷贝,那么可以明确要求。
·没有限制条件的分片表达式(L[:])能够复制序列。
·字典copy方法(X.copy())能够复制字典。
·有些内置函数(例如,list)能够生成拷贝(list(L))。
·copy标准库模块能够生成完整拷贝。
x=[1,2,3,[4,4,4],5,6]
l=[1,x[:],2,3]
d={1:x[:],2:2}
print(x,'\n',l,'\n',d)
x[1]=0
x[3][0]=0
print(x,'\n',l,'\n',d)
运行结果
[1, 2, 3, [4, 4, 4], 5, 6]
[1, [1, 2, 3, [4, 4, 4], 5, 6], 2, 3] {1: [1, 2, 3, [4, 4, 4], 5, 6], 2: 2} [1, 0, 3, [0, 4, 4], 5, 6]
[1, [1, 2, 3, [0, 4, 4], 5, 6], 2, 3] {1: [1, 2, 3, [0, 4, 4], 5, 6], 2: 2} [Program finished]
使用切片后,可以看到l和D并没有因x[1]=0而改变,但细心的读者可以看到,x[3][0]=0仍对结果产生了影响,这就涉及到深拷贝与浅拷贝的问题。
无条件值的分片以及字典copy方法只能做顶层复制。也就是说,不能够复制嵌套的数据结构(如果有的话)。如果你需要一个深层嵌套的数据结构的完整的、完全独立的拷贝,那么就要使用标准的copy模块——包括import copy语句,并编辑X=copy.deepcopy(Y)对任意嵌套对象Y做完整的复制。这一调用语句能够递归地遍历对象来复制它们所有的组成部分。
例如
from copy import deepcopy
x=[1,2,3,[4,4,4],5,6]
l=[1,deepcopy(x),2,3]
d={1:deepcopy(x),2:2}
print(x,'\n',l,'\n',d)
x[1]=0
x[3][0]=0
print(x,'\n',l,'\n',d)
运行结果
[1, 2, 3, [4, 4, 4], 5, 6]
[1, [1, 2, 3, [4, 4, 4], 5, 6], 2, 3] {1: [1, 2, 3, [4, 4, 4], 5, 6], 2: 2} [1, 0, 3, [0, 4, 4], 5, 6]
[1, [1, 2, 3, [4, 4, 4], 5, 6], 2, 3] {1: [1, 2, 3, [4, 4, 4], 5, 6], 2: 2} [Program finished]
可以看到得到了我们想要的结果。
另一个有趣的现象:
x=[1,2,3,4,5,6]
l=x*4
d=[x]*4
print(x,'\n',l,'\n',d)
x[1]=0
print(x,'\n',l,'\n',d)
运行结果
[1, 2, 3, 4, 5, 6]
[1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6]
[[1, 2, 3, 4, 5, 6], [1, 2, 3, 4, 5, 6], [1, 2, 3, 4, 5, 6], [1, 2, 3, 4, 5, 6]]
[1, 0, 3, 4, 5, 6]
[1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6]
[[1, 0, 3, 4, 5, 6], [1, 0, 3, 4, 5, 6], [1, 0, 3, 4, 5, 6], [1, 0, 3, 4, 5, 6]] [Program finished]
由于X在第二次重复中是嵌套的,d结束嵌套的引用,返回赋值为X的原始列表,所以出现了与上一节提到的相同类型的副作用。
解决这一问题的方法与前面所讲过的一样,因为这的确是以另一种生成共享可变对象引用的方法。如果你还记得重复、合并以及分片只是在复制操作数对象的顶层的话,这类情况就比较好理解了。
留意循环数据结构
如果遇到一个复合对象包含指向自身的引用,就称之为循环对象。无论何时Python在对象中检测到循环,都会打印成[...],而不会陷入无限循环。
x=[1,2,3,4,5,6]
x.append(x)
print(x)
结果
[1, 2, 3, 4, 5, 6, [...]]
[Program finished]
总结:
赋值生成引用,而不是拷贝
这种影响通常只是在大型程序中才显得重要,而共享引用往往就是你真正想要的。如果不是这样,你可以明确地对它们进行拷贝以避免对象共享。