python
a=123
b=a
c=b
很多新手朋友没接触深拷贝、浅拷贝之前,如果没有深入了解python的对象引用和内存管理,可能就会误以为上述代码中b变量就是备份了a,变量c是备份了b。实则不然,b跟c都只是引用了a所指向的同一份数据。接下来让咱们一起深入探究一下其原理。
目录
[二、= 赋值](#二、= 赋值)
[三、浅拷贝 copy() 和深拷贝 deepcopy()](#三、浅拷贝 copy() 和深拷贝 deepcopy())
一、对象引用和内存管理的概念
对象引用和内存管理是Python编程中的核心概念,而想要吃透浅拷贝、深拷贝,就得先了解对象引用和内存管理是什么意思。在实际编程过程中,理解这些概念有助于我们编写更高效、无内存泄漏的代码。我们分开来讲:
1、对象引用:
在Python中,变量并不像其他语言那样直接存储值,而是存储一个指向对象的引用或地址。当创建一个对象(例如字符串、列表、字典等)时,Python会在内存中分配一块区域来存放这个对象的内容,然后将该区域的内存地址赋给变量。因此,当我们操作变量时,实际上是在通过变量访问内存中的实际对象。
python
# 创建一个列表对象
list_obj = [1, 2, 3]
# 另一个变量指向同一个列表对象
another_list = list_obj
print(another_list) # 输出:[1, 2, 3]
print(id(list_obj)==id(another_list)) # 输出:True 确认它们指向同一块内存空间
# 改变list_obj的内容
list_obj.append(4)
# 再次查看another_list的内容
print(another_list) # 输出:[1, 2, 3, 4]
print(id(list_obj)==id(another_list)) # 输出:True 确认它们指向同一块内存空间
'''
上述例子中,another_list并不是创建了新的列表,而是引用了 list_obj 所指向的同一份数据。
因此,当修改 list_obj 的内容时,another_list显示的内容也会随之改变。
'''
2、内存管理:
内存管理中的垃圾回收机制是一个自动过程,它负责检测和回收不再被程序使用的对象所占用的内存。我们无法直接观察到垃圾回收的具体时刻,Python的垃圾回收机制一般都会自动解决,对咱们来说弄不弄明白这点不是刚需。
在生活中,我们可以用图书馆借阅书籍的例子来解释对象引用和内存管理的概念:
对象与书籍: 在Python中,每个创建的对象就像图书馆中的每一本书。当你新买一本书(创建一个对象)时,它会被放在书架上占用一定的空间(分配内存)。这本书有自己的唯一标识(就如同对象在内存中的地址)。
引用与借书证: 读者的借书证可以类比为程序中的变量或引用。当你从图书馆借书时,图书馆会给你一张借书证,上面记录了你所借书籍的信息。同样地,在编程中,当你将一个对象赋值给一个变量时,这个变量就相当于一张"借书证",它指向了那本特定的"书"(即对象在内存中的位置)。
内存管理与图书归还: Python解释器通过引用计数机制来管理这些"书籍"。每张借书证对应一本书,图书馆就会统计该书被借出多少次(引用计数加1)。当读者归还书籍时(即删除对对象的引用或变量超出作用域),图书馆会注销一张借书证(引用计数减1)。
垃圾回收与清理书架: 如果某本书的所有借书证都被注销了(引用计数为0),这意味着没有任何人还在阅读这本书,图书馆就可以把这本书下架(垃圾回收机制回收对象占据的内存)。这样既能保证有限的书架空间得到充分利用,又不会丢失仍在使用的书籍。
二、= 赋值
在Python中,赋值操作通过等号 =
完成,接下来我们再来看看文章开头的那段简单代码,一起看看 a、b、c是什么关系:
python
a = 123
b = a
c = b
# a 和 b 、 c 是什么关系?
#在Python中,is关键字用来判断两个对象是否是同一个对象,也就是说它们是否指向内存中的同一块地址。
print(a is c,a is b,c is b) # 输出:True True True
print(id(123)) # 输出:2227363188784
print(id(a)) # 输出:2227363188784
print(id(b)) # 输出:2227363188784
print(id(c)) # 输出:2227363188784
'''
需要注意,id()函数可以用来查看变量所指向对象的内存地址,
每次代码运行完后内存会回收,所以id()函数的结果会每次运行时有所不同
但是 a、b、c 的id()结果每次运行都会相同,因为他们指向同一个对象 123
'''
如上所示,赋值就是将一个值(或对象)与一个变量名通过 等于号= 关联起来。等于号= 并不是在创建新的变量,本质上只是为同一个变量或对象取了不同的名字,所以等于号= 不能用来备份数据,那我们需要备份数据该咋操作呢,我们接着往下走。
三、浅拷贝 copy() 和深拷贝 deepcopy()
浅拷贝copy():(shallow copy)浅拷贝是指创建一个新的对象,但这个新对象只复制了原对象的引用而不是复制其包含的所有元素。换言之,对于不可变类型的元素(如整数、字符串、元组),浅拷贝会复制一份相同的值;而对于可变类型(如列表、字典),浅拷贝只会复制这些容器对象本身,而不会复制它们所包含的子对象。
深拷贝deepcopy(): (deep copy)深度拷贝与浅拷贝不同,深度拷贝不仅会复制原对象本身,还会递归地复制原对象所包含的所有元素以及它们的子元素,直至所有可到达的对象都被复制为止。也就是说,深度拷贝创建了一个完全独立的新对象,新对象及其包含的子对象都是原始对象的副本,对其中一个进行修改不会影响到另一个。
了解完两种拷贝我们接着代码详细演示一下:
python
import copy
#1、不可变数据类型的copy
a='123445'
b=a # 赋值
shallow=copy.copy(a) # 浅拷贝
deep=copy.deepcopy(a) # 深拷贝
print(f"a==b==shallow==deep: {a==b==shallow==deep}")
# 输出:a==b==shallow==deep: True 它们的值都相同
print(f"id(a)==id(b)==id(shallow)==id(deep):{id(a)==id(b)==id(shallow)==id(deep)}")
#输出:id(a)==id(b)==id(shallow)==id(deep):True 它们id相同
print(a is b is shallow is deep) # 输出:True
'''
不可变的数据类型,等于赋值、浅复制、深复制id都是一样的
'''
#我们接着分别对a、shallow、deep进行操作,观察它们的变化
a.append(4)
shallow[0]='被修改了'
deep.extend(['新扩展一个列表'])
print(
f'''
a--> {a},
b--> {b},
shallow--> {shallow},
deep--> {deep}
''')
#输出如下:可见b列表受a的改变影响而改变,shallow和deep列表都是独立变化没有影响到被复制对象
'''
a--> [1, 2, 3, '456', 4],
b--> [1, 2, 3, '456', 4],
shallow--> ['被修改了', 2, 3, '456'],
deep--> [1, 2, 3, '456', '新扩展一个列表']
'''
#2、可变数据类型(列表、字典)的copy(元素不包含复杂元素)
a=[1,2,3,'456']
b=a # 赋值
shallow=copy.copy(a) # 浅拷贝
deep=copy.deepcopy(a) # 深拷贝
print(f"a==b==shallow==deep: {a==b==shallow==deep}")
# 输出:a==b==shallow==deep: True 它们的值都相同
print(f"id(a)==id(b)==id(shallow)==id(deep):{id(a)==id(b)==id(shallow)==id(deep)}")
# 输出:id(a)==id(b)==id(shallow)==id(deep):False 这时id不同了
print(id(a),id(b),id(shallow),id(deep))
# 输出:1974935889792 1974935889792 1974935890368 1974936294272
'''
从输出结果来看:赋值操作a、b的id还是相同,但是shallow、deep的id却不同了,
深拷贝和浅拷贝对可变数据类型进行操作时,是新建了一个内存空间,然后将a的内容复制进去,
虽然内容相同但是id不同了,浅拷贝和深拷贝的对象都是独立的新的对象
'''
#我们接着分别对a、shallow、deep进行操作,观察它们的变化
a.append(4)
shallow[0]='被修改了'
deep.extend(['新扩展一个列表'])
print(
f'''
a--> {a},
b--> {b},
shallow--> {shallow},
deep--> {deep}
''')
#输出如下:可见b列表受a的改变影响而改变,shallow和deep列表都是单独变化没有影响到被复制对象
'''
a--> [1, 2, 3, '456', 4],
b--> [1, 2, 3, '456', 4],
shallow--> ['被修改了', 2, 3, '456'],
deep--> [1, 2, 3, '456', '新扩展一个列表']
'''
#3、可变数据类型包含复杂元素的copy
dic1= {'int1':1,'int2':2,'int3':3,'list1':['小石','小杨','荒'],'int4':111}
dic2=dic1
dic_shallow=copy.copy(dic1)
dic_deep=copy.deepcopy(dic1)
print(id(dic1),id(dic2),id(dic_shallow),id(dic_deep))
# 输出:2003633633984 2003633633984 2003633633792 2003633667392
# 还是一样,可变数据含复杂元素的拷贝,浅拷贝和深拷贝都是新建了一个内存空间,然后将list1的内容复制进去
# 赋值只是改了各名字,指向的变量指向的内存地址都相同
print(dic1==dic2==dic_shallow==dic_deep) # 输出:True 所有的值都是相同的
# 接下来我们分别操作,观察它们的变化
#(1)---改变被复制对象dic1内的列表value
dic1['list1'].append('小明')
print(f'''
dic1--> {dic1},
dic2--> {dic2},
dic_shallow--> {dic_shallow},
dic_deep--> {dic_deep}
''')
'''
dic1--> {'int1': 1, 'int2': 2, 'int3': 3, 'list1': ['小石', '小杨', '荒', '小明'], 'int4': 111},
dic2--> {'int1': 1, 'int2': 2, 'int3': 3, 'list1': ['小石', '小杨', '荒', '小明'], 'int4': 111},
dic_shallow--> {'int1': 1, 'int2': 2, 'int3': 3, 'list1': ['小石', '小杨', '荒', '小明'], 'int4': 111},
dic_deep--> {'int1': 1, 'int2': 2, 'int3': 3, 'list1': ['小石', '小杨', '荒'], 'int4': 111}
1809491884736 1809491884736 1809491884544 1809491918144
'''
print(id(dic1),id(dic2),id(dic_shallow),id(dic_deep))
#看输出结果,dic1、dic2、dic_shallow的值都发生了变化,dic_deep的值没有变化
#改变被复制对象内的复杂元素(可变数据类型)时,浅拷贝会受被复制对象影响改变,但深拷贝不会
#(2)---改变浅复制对象dic_shallow内的列表value
dic_shallow['list1'][2]='荒天帝'
print(f'''
dic1--> {dic1},
dic2--> {dic2},
dic_shallow--> {dic_shallow},
dic_deep--> {dic_deep}
''')
print(id(dic1),id(dic2),id(dic_shallow),id(dic_deep))
'''
dic1--> {'int1': 1, 'int2': 2, 'int3': 3, 'list1': ['小石', '小杨', '荒天帝', '小明'], 'int4': 111},
dic2--> {'int1': 1, 'int2': 2, 'int3': 3, 'list1': ['小石', '小杨', '荒天帝', '小明'], 'int4': 111},
dic_shallow--> {'int1': 1, 'int2': 2, 'int3': 3, 'list1': ['小石', '小杨', '荒天帝', '小明'], 'int4': 111},
dic_deep--> {'int1': 1, 'int2': 2, 'int3': 3, 'list1': ['小石', '小杨', '荒'], 'int4': 111}
1809491884736 1809491884736 1809491884544 1809491918144
'''
#我们看到,改变浅复制对象内的复杂元素(可变数据类型)时,被复制对象也会改变,但是深复制对象不会改变
# (3)---改变深复制对象dic_deep内的列表value
dic_deep['list1'].append('火女')
print(f'''
dic1--> {dic1},
dic2--> {dic2},
dic_shallow--> {dic_shallow},
dic_deep--> {dic_deep}
''')
print(id(dic1),id(dic2),id(dic_shallow),id(dic_deep))
'''
dic1--> {'int1': 1, 'int2': 2, 'int3': 3, 'list1': ['小石', '小杨', '荒天帝', '小明'], 'int4': 111},
dic2--> {'int1': 1, 'int2': 2, 'int3': 3, 'list1': ['小石', '小杨', '荒天帝', '小明'], 'int4': 111},
dic_shallow--> {'int1': 1, 'int2': 2, 'int3': 3, 'list1': ['小石', '小杨', '荒天帝', '小明'], 'int4': 111},
dic_deep--> {'int1': 1, 'int2': 2, 'int3': 3, 'list1': ['小石', '小杨', '荒', '火女'], 'int4': 111}
1809491884736 1809491884736 1809491884544 1809491918144
'''
#我们看到,改变深复制对象内的复杂元素(可变数据类型)时,被复制对象不会改变,浅复制对象也不会改变
# (4)---改变被赋值对象dic1内的一般元素(不可变类型数据)
dic1.setdefault('list2',['新列表'])
print(f'''
dic1--> {dic1},
dic2--> {dic2},
dic_shallow--> {dic_shallow},
dic_deep--> {dic_deep}
''')
print(id(dic1),id(dic2),id(dic_shallow),id(dic_deep))
'''
dic1--> {'int1': 1, 'int2': 2, 'int3': 3, 'list1': ['小石', '小杨', '荒天帝', '小明'], 'int4': 111, 'list2': ['新列表']},
dic2--> {'int1': 1, 'int2': 2, 'int3': 3, 'list1': ['小石', '小杨', '荒天帝', '小明'], 'int4': 111, 'list2': ['新列表']},
dic_shallow--> {'int1': 1, 'int2': 2, 'int3': 3, 'list1': ['小石', '小杨', '荒天帝', '小明'], 'int4': 111},
dic_deep--> {'int1': 1, 'int2': 2, 'int3': 3, 'list1': ['小石', '小杨', '荒', '火女'], 'int4': 111}
1809491884736 1809491884736 1809491884544 1809491918144
'''
#改变被复制对象内的不可变类型数据时,只有赋值对象会改变,浅复制对象和深复制对象都不会改变
#从始至终,每个对象的id没有改变过
copy()与deepcopy()之间的主要区别是python对数据的存储方式
深复制将被复制对象完全再复制一遍作为独立的新个体单独存在,改变原有被复制对象不会对已经复制出来的新对象产生影响。
浅复制 要分两种情况进行讨论:
- 当浅复制的值是不可变对象(数值,字符串,元组)时和"等于赋值"的情况一样
- 当浅复制的值是可变对象(列表,字典)时会产生一个"不是那么独立的对象"存在。有两种情况:
- 第一种情况:复制的对象中无复杂子对象 ,原来值的改变并不会影响浅复制的值,同时浅复制的值改变也并不会影响原来的值
- 第二种情况:复制的对象中有复杂子对象 (例如列表中的一个子元素是一个可变数据类型--列表),如果改变其中复杂子对象(无论是改变被复制对象,还是改变浅复制对象中的可变类型数据),都会相互造成影响
四、总结
1、改变被复制对象的一般元素时,浅复制对象没有改变,深复制对象没有改变;
2、改变被复制对象的复杂子项(可变数据类型如列表、字典)中的元素时,浅复制对象跟着改变了,深复制对象没改变;
3、改变浅复制对象的一般项时,被复制对象没有改变,深复制对象没有改变;
4、改变浅复制对象的复杂子项(可变数据类型如列表、字典)内的元素时,被复制对象改变了,深复制对象没有改变;
5、改变深复制对象的一般项时,被复制对象没有改变,浅复制对象没有改变;
6、改变深复制对象的复杂子项(可变数据类型如列表、字典)内的元素时,被复制对象没有改变,浅复制对象没有改变。
由上述结论我们可以得出它们的使用场景:
彼此隔离没有关联的数据、数据量不大,需要拷贝的数据完全独立,使用深拷贝
彼此有关联的数据、数据量较大,需要跟踪得到完整数据,使用 浅拷贝
希望该篇文章的内容能够有效帮助各位友友理解使用深拷贝跟浅拷贝