Python自学 - 引用与拷贝探索(防坑关键知识)
1 Python自学 - 引用与拷贝探索(防坑关键知识)
1.1 概述
读者如果学习过C/C++语言,对引用的概念应该会比较了解,引用本质是将地址共享给另一个变量,两个变量指向同一个内存地址,任何一个变量的修改都会在另一个变量上即时生效(实际都是一个内存)。
1.2 引用探索
1.2.1 简单结构是否存在引用
- 结论:
- 简单数据结构:整型、浮点型、字符串,在变量给变量赋值时存在共享内存现象,但变量修改后即分裂成两个完全不同的对象。
- 元组:行为与简单变量相同。
- 复杂数据结构:列表、字典、集合,变量对变量赋值时共享内存,对任一变量使用成员函数进行修改,所有变量都同步修改(同一地址),但对变量使用同类常量对象赋值时,变量分裂成完全新的变量。
1.2.1.1 整型:变量赋值存在引用,修改后分裂成不同对象
- 整型变量赋值给另一个变量后,两个变量的值、地址都相同,表示这里存在引用。
var1 = 10
var2 = var1 #将变量var1赋值给变量var2
print(f'var1: {var1}, addr1:{id(var1)}, var2: {var2}, addr2:{id(var2)}')
#输出:var1: 10, addr1:140732679062728, var2: 10, addr2:140732679062728
- 修改变量值后,完全分裂成两个不同的对象
var1 = 10
var2 = var1 #将变量var1赋值给变量var2,两个变量都指向了同一个地址
print(f'var1: {var1}, addr1:{id(var1)}, var2: {var2}, addr2:{id(var2)}')
#输出:var1: 10, addr1:140732679062728, var2: 10, addr2:140732679062728
var2 = 30 #变量2的值修改后,得到了新的地址,与变量1分裂成两个不同的对象
print(f'var1: {var1}, addr1:{id(var1)}, var2: {var2}, addr2:{id(var2)}')
#输出:var1: 10, addr1:140732679062728, var2: 30, addr2:140732679063368
1.2.1.2 浮点型:变量赋值存在引用,修改后分裂成不同对象
浮点型变量的情况与整型相同,变量赋值时共享内存地址。
var1 = 10.011
var2 = var1 #将变量var1赋值给变量var2,两个变量都指向了同一个地址
print(f'var1: {var1}, addr1:{id(var1)}, var2: {var2}, addr2:{id(var2)}')
#输出:var1: 10.011, addr1:2038061764976, var2: 10.011, addr2:2038061764976
var2 = 30.67 #变量2的值修改后,得到了新的地址,与变量1分裂成两个不同的对象
print(f'var1: {var1}, addr1:{id(var1)}, var2: {var2}, addr2:{id(var2)}')
#输出:var1: 10.011, addr1:2038061764976, var2: 30.67, addr2:2038060456432
1.2.1.3 字符串:变量赋值存在引用,修改后分裂成不同对象
字符串变量情况与整型相同,变量赋值时共享内存地址
var1 = "python"
var2 = var1 #将变量var1赋值给变量var2,两个变量都指向了同一个地址
print(f'var1: {var1}, addr1:{id(var1)}, var2: {var2}, addr2:{id(var2)}')
#输出:var1: python, addr1:2733226584192, var2: python, addr2:2733226584192
var2 = "javascript" #变量2的值修改后,得到了新的地址,与变量1分裂成两个不同的对象
print(f'var1: {var1}, addr1:{id(var1)}, var2: {var2}, addr2:{id(var2)}')
#输出:var1: python, addr1:2733226584192, var2: javascript, addr2:2733228213936
1.2.2 复杂结构的引用
复杂结构研究对象为:列表、元组、字典、集合。
1.2.2.1 列表:两个变量的赋值是共享地址,任一变量修改都会同时生效
列表变量使用另一列表变量赋值后,两个列表变量指向同一地址,且任何一个变量对列表的修改都会影响到两个列表变量(限于成员函数修改),如果使用列表对象赋值,则两个变量会分裂。
- 使用成员函数修改变量,两个变量都会同步更新(实际是一个地址)
var1 = [1, 2, 3]
var2 = var1 #将变量var1赋值给变量var2,两个变量都指向了同一个地址
print(f'var1: {var1}, addr1:{id(var1)}, var2: {var2}, addr2:{id(var2)}')
#输出:var1: [1, 2, 3], addr1:2086805069888, var2: [1, 2, 3], addr2:2086805069888
var2.extend([4, 5]) #变量2的值修改后,地址没有变化,且查看变量1和变更2时,两个列表的数据都变化且值相同
print(f'var1: {var1}, addr1:{id(var1)}, var2: {var2}, addr2:{id(var2)}')
#输出:var1: [1, 2, 3, 4, 5], addr1:2086805069888, var2: [1, 2, 3, 4, 5], addr2:2086805069888
- 使用初始化赋值后,两个变量会分裂成两个不同对象
var1 = [1, 2, 3]
var2 = var1 #将变量var1赋值给变量var2,两个变量都指向了同一个地址
print(f'var1: {var1}, addr1:{id(var1)}, var2: {var2}, addr2:{id(var2)}')
#输出:var1: [1, 2, 3], addr1:2363003985984, var2: [1, 2, 3], addr2:2363003985984
var2 = [1, 2, 3, 4, 5] #变量2赋值后,地址变化,两个变量完全分裂成两个对象
print(f'var1: {var1}, addr1:{id(var1)}, var2: {var2}, addr2:{id(var2)}')
#输出:var1: [1, 2, 3], addr1:2363003985984, var2: [1, 2, 3, 4, 5], addr2:2363005524544
1.2.2.2 元组:两个变量赋值时共享地址,修改后分裂为不同对象
由于元组的成员是无法变化的,所以元组的行为和简单数据类型相同。
var1 = (1, 2, 3)
var2 = var1 #将变量var1赋值给变量var2,两个变量都指向了同一个地址
print(f'var1: {var1}, addr1:{id(var1)}, var2: {var2}, addr2:{id(var2)}')
#输出:var1: (1, 2, 3), addr1:1219793071552, var2: (1, 2, 3), addr2:1219793071552
var2 = (1, 2, 3, 4, 5) #变量2赋值后,地址变化,两个变量完全分裂成两个对象
print(f'var1: {var1}, addr1:{id(var1)}, var2: {var2}, addr2:{id(var2)}')
#输出:var1: (1, 2, 3), addr1:1219793071552, var2: (1, 2, 3, 4, 5), addr2:1219793104032
1.2.2.3 字典:两个变量的赋值是共享地址,任一变量修改都会同时生效
字典的行为与列表相同,两个变量赋值时共享地址,指向同一个地址,修改一个变量,另一个变量也同步生效(成员函数),但使用字典对象赋值后,变量分裂成完全新的变量。
var1 = {"key1":1, "key2":2}
var2 = var1 #将变量var1赋值给变量var2,两个变量都指向了同一个地址
print(f'var1: {var1}, addr1:{id(var1)}, var2: {var2}, addr2:{id(var2)}')
#输出:var1: {'key1': 1, 'key2': 2}, addr1:2513141032320, var2: {'key1': 1, 'key2': 2}, addr2:2513141032320
var2.update(x=1, y=2) #变量2更新后,地址不变,两个变量内容完全相同
print(f'var1: {var1}, addr1:{id(var1)}, var2: {var2}, addr2:{id(var2)}')
#输出:var1: {'key1': 1, 'key2': 2, 'x': 1, 'y': 2}, addr1:2513141032320, var2: {'key1': 1, 'key2': 2, 'x': 1, 'y': 2}, addr2:2513141032320
var2 = {"key3":3} #变量2赋值后,地址变化,两个变量完全分裂成两个对象
print(f'var1: {var1}, addr1:{id(var1)}, var2: {var2}, addr2:{id(var2)}')
#输出:var1: {'key1': 1, 'key2': 2, 'x': 1, 'y': 2}, addr1:2513141032320, var2: {'key3': 3}, addr2:2513141427456
1.2.2.4 集合:两个变量的赋值是共享地址,任一变量修改都会同时生效
集合的行为与列表相同,两个变量赋值时共享地址,指向同一地址,使用成员变量修改变量,另一个变量也同步生效,但对变量使用集合变量赋值时,变量分裂成完全新的变量。
var1 = {1, 2}
var2 = var1 #将变量var1赋值给变量var2,两个变量都指向了同一个地址
print(f'var1: {var1}, addr1:{id(var1)}, var2: {var2}, addr2:{id(var2)}')
#输出:var1: {1, 2}, addr1:2276059562720, var2: {1, 2}, addr2:2276059562720
var2.update([3,4]) #变量2赋值后,地址不变,两个变量数据同时生效(同一地址)
print(f'var1: {var1}, addr1:{id(var1)}, var2: {var2}, addr2:{id(var2)}')
#输出:var1: {1, 2, 3, 4}, addr1:2276059562720, var2: {1, 2, 3, 4}, addr2:2276059562720
var2 = {6,7} #变量2赋值后,地址变化,两个变量完全分裂成两个对象
print(f'var1: {var1}, addr1:{id(var1)}, var2: {var2}, addr2:{id(var2)}')
#输出:var1: {1, 2, 3, 4}, addr1:2276059562720, var2: {6, 7}, addr2:2276059879616
1.3 拷贝的使用
简单变量、元组无所谓拷贝,其中,元组成员是不可变的,更谈不上拷贝。列表、字典、集合的拷贝,主要是防止变量给变量赋值后,其中一个变量修改了对象的数据,另外一个变量无法预知数据的修改,导致程序出现混乱,因此,需要做修改隔离。
1.3.1 列表的拷贝
使用copy
进行浅拷贝后,列表变量地址指向不同,但成员却指向同一地址,这是浅拷贝的精髓:节省内存。但修改成员变量后,对应的成员分裂成新的对象(值不同了)。
从以上行为来看,列表的浅拷贝对原列表变量似乎不存在影响。
var1 = [1,2]
var2 = var1.copy()
print(f"var1:{var1},addr1:{id(var1)}, addr1-1: {id(var1[0])}; var2:{var2}, addr2:{id(var2)}, addr2-1:{id(var2[0])}")
#输出:var1:[1, 2],addr1:1615825887296, addr1-1: 140733906224040; var2:[1, 2], addr2:1615826034944, addr2-1:140733906224040
var1.extend([3,4])
print(f"var1:{var1},addr1:{id(var1)}; var2:{var2}, addr2:{id(var2)}")
#输出:var1:[1, 2, 3, 4],addr1:1615825887296; var2:[1, 2], addr2:1615826034944
var1[0] = 9
print(f"var1:{var1},addr1:{id(var1)}; var2:{var2}, addr2:{id(var2)}")
#输出:var1:[9, 2, 3, 4],addr1:1615825887296; var2:[1, 2], addr2:1615826034944
1.3.2 字典的拷贝
copy
进行浅拷贝后,行为与列表的浅拷贝类似,python会尽量节省内存,对象指向不同地址,但成员指向同一地址,在对成员进行修改时,成员分裂到新的地址,不影响原变量的成员。
var1 = {'x':1, 'y':2}
var2 = var1.copy()
print(f"var1:{var1},addr1:{id(var1)}, addr1-1: {id(var1['x'])}; var2:{var2}, addr2:{id(var2)}, addr2-1:{id(var2['x'])}")
#输出:var1:{'x': 1, 'y': 2},addr1:1522714666368, addr1-1: 140733906224040; var2:{'x': 1, 'y': 2}, addr2:1522715059904, addr2-1:140733906224040
var1.update(z=3)
print(f"var1:{var1},addr1:{id(var1)}; var2:{var2}, addr2:{id(var2)}")
#输出:var1:{'x': 1, 'y': 2, 'z': 3},addr1:1522714666368; var2:{'x': 1, 'y': 2}, addr2:1522715059904
var1['x'] = 5
print(f"var1:{var1},addr1:{id(var1)}; var2:{var2}, addr2:{id(var2)}")
#输出:var1:{'x': 5, 'y': 2, 'z': 3},addr1:1522714666368; var2:{'x': 1, 'y': 2}, addr2:1522715059904
1.3.3 集合的拷贝
copy
浅拷贝后,对象指向不同的地址,但由于集合无法索引到成员,这里看不到两个变量的成员的地址情况,但这些成员的地址都是共享的。
var1 = {1, 2}
var2 = var1.copy()
print(f"var1:{var1},addr1:{id(var1)}; var2:{var2}, addr2:{id(var2)}")
#输出:var1:{1, 2},addr1:1541781897952; var2:{1, 2}, addr2:1541782214848
var1.update({3})
print(f"var1:{var1},addr1:{id(var1)}; var2:{var2}, addr2:{id(var2)}")
#输出:var1:{1, 2, 3},addr1:1541781897952; var2:{1, 2}, addr2:1541782214848
注:由于集合的成员都是不可变对象,浅拷贝在集合上不会带来太坏的影响。
1.3.4 浅拷贝带来的坑
浅拷贝这么好,可以节省内存,那副作用是什么呢? 副作用恐怕无法接受!
以列表为例,由于列表的成员可以是任意类型,那当然也可以是列表类型!如下的示例中,列表的第1个元素是列表类型,浅拷贝过后,对var1
的第1个成员又增加了8,9两个数,从结果可以看到,var1
和var2
的第1个成员数据都被修改了!
这种坑导致的bug会坑到死~
var1 = [[1],2,3]
var2 = var1.copy()
print(f"var1:{var1},addr1:{id(var1)}; var2:{var2}, addr2:{id(var2)}")
#输出:var1:[[1], 2, 3],addr1:3105159749888; var2:[[1], 2, 3], addr2:3105160760000
var1[0].extend([8,9])
print(f"var1:{var1},addr1:{id(var1)}; var2:{var2}, addr2:{id(var2)}")
#输出:var1:[[1, 8, 9], 2, 3],addr1:3105159749888; var2:[[1, 8, 9], 2, 3], addr2:3105160760000
1.3.5 深拷贝
既然浅拷贝这么坑,那怎么办呢? python
提供了深拷贝
,完全克隆一份新的对象,不再考虑节省内存,从些两个变量是路人,互不影响!
import copy
var1 = [[1],2,3]
var2 = copy.deepcopy(var1)
print(f"var1:{var1},addr1:{id(var1)}; var2:{var2}, addr2:{id(var2)}")
#输出:var1:[[1], 2, 3],addr1:2852169539648; var2:[[1], 2, 3], addr2:2852171136896
var1[0].extend([8,9])
print(f"var1:{var1},addr1:{id(var1)}; var2:{var2}, addr2:{id(var2)}")
#输出:var1:[[1, 8, 9], 2, 3],addr1:2852169539648; var2:[[1], 2, 3], addr2:2852171136896
注:使用深拷贝需要导入copy
模块,然后调用deepcopy()
函数。
从上文中的示例,对于列表、字典对象,浅拷贝时成员还存在地址共享现象,此时修改成员可能会导致浅拷贝出来的对象都受影响,这种非预料的修改,可能会给程序带来灾难性的影响! 因此,必须养成良好的代码习惯,慎用浅拷贝!
作者声明:本文用于记录和分享作者的学习心得,水平有限,难免存在表达错误,欢迎交流和指教!
Copyright © 2022~2024 All rights reserved.