Python中的赋值、浅拷贝和深拷贝

本文详细解析Python中可变与不可变类型的赋值、浅拷贝和深拷贝的区别,通过实例展示不同拷贝方式下对象ID的变化,揭示Python资源管理的精髓。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本章节内容涉及可变类型和不可变类型的基础知识,详见以下文章:
https://blog.youkuaiyun.com/matlab2007/article/details/102831025

1. 当对不可变类型进行复制操作时

准则:不可变类型变量的复制,一个变,另外一个永远不跟着变

import copy
a = 'hello world'
b = a
c = copy.copy(a)
d = copy.deepcopy(a)
print(id(a),id(b),id(c),id(d))

运行结果:
322518536
322518536
322518536
322518536
copy和deepcopy的作用没有区别。语句b = a执行后,虽然a和b的值和id一样,但将来修改其中任何一个,另外一个均不受影响。如果该处不明白,请参考可变类型和不可变类型的基础知识 。

2. 当对可变类型变量进行操作时

2.1 可变类型对象不包含复杂子对象

即可变类型的子对象为数字、字符串、布尔值等不可变类型组成,不包含列表、字典、集合等可变类型复杂子对象。

初始化并进行复制:

a = [100,200,300]
b = a
c = copy.copy(a)
d = copy.deepcopy(a)
print(id(a),id(b),id(c),id(d)) 

运行结果:
323457272
323457272
322518112
322517352
上面的代码运行结果可以看出,copy和deepcopy复制之后的变量id一致均与原id不一样。下面测一下子对象的id值。

a = [100,200,300]
b = a
c = copy.copy(a)
d = copy.deepcopy(a)
print(id(a[0]),id(b[0]),id(c[0]),id(d[0])) 

运行结果:505988168 505988168 505988168 505988168
运行结果可能超出想象,a中不可变的子对象“100”,历经赋值、浅拷贝、深拷贝后,竟然都是指内存中同一个“100”。

细想一下,其实Python这么做是对的,因为虽然外层是一个列表,但内层是不可变的对象,不可变对象为什么还要复制多份放内存不同地方呢?c和d外层列表的id和a不一样,但内层的子对象因为都是不可变对象,以后如果需要修改,反正原地修改无效,肯定要重新创建子对象,所以内层不可变子对象存一份即可。

尤其是子对象元素很多的时候,比如:
a = [i for i in range(10000000)]
b = a
如果Python不这么做,为了创建b得消耗多少内存呀?

那为啥d = copy.deepcopy(a)后,id[d[0])也和id(a[0])一样呢?不是有的教材上说深拷贝后的对象和原对象一点关系没有吗?
初学者认为深拷贝后的对象和原对象一点关系没有,完全是两个毫无关系的对象,那就too naive了。深拷贝和赋值相比,虽然拷贝前后的两个对象使用起来似乎没有没有任何关系,当在一个可变类型变量内部存在不可变类型子对象时,即使是深拷贝, Python不会重复再另存一份相同的不可变子对象,那样的话就太foolish了。(牢记:不可变的东西不要重复放多份!

情况1:= 赋值后的修改测试

b[0] = 0
print(b)
print(a)
print(id(a[0]) == id(b[0]))

运行结果:
[0, 200, 300]
[0, 200, 300]
True

赋值 = 操作后,修改b的子对象后,b[0]指向了内存中的存放数据“0”的新地址,然后a[0]的指向也发生变化,指向同一个“0”位置,a[0]的值也发生变化。由于a[0]和b[0]的指向同步修改,id值也始终如一。

情况2:浅拷贝后的修改测试

a = [100,200,300]
c = copy.copy(a)
c[0] = 10
print(c)
print(a)
print(id(a[0]) == c[0])

运行结果:
[10, 200, 300]
[100, 200, 300]
False

情况3:深拷贝后的修改测试:

import copy
a = [100,200,300]
c = copy.deepcopy(a)
c[0] = 10
print(c)
print(a)
print(id(a[0]) == c[0])

运行结果:
[10, 200, 300]
[100, 200, 300]
False

小结:深拷贝后一个变另外一个不跟着变,与浅拷贝一致

一般的教材讨论到这里就完事了,从此你得到一个理解:[1, 2, 3, 4] 这种变量,b=a 赋值后是引用赋值,修改a后b跟着变;浅拷贝后一个变另一个不变;深拷贝不知道是啥东西,效果跟浅拷贝类似,好像说深拷贝后的东西彻底跟原来的东西一毛钱关系没有了!

恭喜你,能说出以上差别,基本到达了60分水平!混个二级证貌似没有问题。但希望计算机科学与技术和软件工程专业的同学往下看看,非计算机专业同学可以关网页走人了。注意上面说的修改指的是“原地修改”。

===============================================================================================

华丽的分割线:

2.2 可变类型对象包含复杂子对象

情况1:= 赋值后的修改测试

a = [[1,2,3],[4,5,6]]
b = a
print(id(a) == id(b))

结果为:True

a[0].append(100)
print(a)
print(b)

结果为:
[[1, 2, 3, 100], [4, 5, 6]]
[[1, 2, 3, 100], [4, 5, 6]]

小结:Python中的 “=“ 赋值除非对不可变类型变量操作,其它所有情况一律是一个变,另外一个跟着变。

情况2:浅拷贝后的修改测试

a = [[1,2,3],[4,5,6]]
b = copy.copy(a)
print(id(a) == id(b))
print(id(a[0]) == id(b[0]))

结果为:
False
True

a = [[1,2,3],[4,5,6]]
b = copy.copy(a)
b[0].append('c')
print(a)

结果为:[[1, 2, 3, ‘c’], [4, 5, 6]]

小节:当一个可变对象a中包含了可变类型的子对象时,用b = copy.copy(a) 浅拷贝后,虽然a和b的id不同,但a和b中的子对象的id是一样的,这样a和b就不是完全独立的,当对a或b进行成员的append和remove操作或把子对象换成不可变类型时,两者不相互影响。但进行对子对象原地修改时,一个的改变会影响另一个。

在python列表部分,可以用b = a[:] 这种切片操作进行复制,此时是赋值还是浅拷贝还是深拷贝呢?测试如下:

a = [[100],[200],[300]]
b = a[:]
print(id(a) == id(b))
print(id(a[0]) == id(b[0]))

运行结果:
False
True
结果说明[:] 这种列表切片操作是“浅拷贝”。修改一下列表内部的子对象,看看另一个对象是否跟着变化

a[0].append(200)
print(b)

运行结果:
[[100, 200], [200], [300]]

情况3:深拷贝后的修改测试:
可变类型对象包含复杂子对象,当用copy.deepcopy时,不管该对象包含的子对象是可变还是不可变的,最终深拷贝后的两个对象均复制独立,无任何相互影响。

a = [[1,2,3],[4,5,6]]
b = copy.deepcopy(a)
b[0].append('c')
print(a)
print(b)

运行结果:
[[1, 2, 3], [4, 5, 6]]
[[1, 2, 3, ‘c’], [4, 5, 6]]

那为什么深拷贝能做到这一点呢,测试如下:

a = [[1,2,3],[4,5,6]]
print(id(a[0]) == id(b[0]))

运行结果:
False
可以看出,deepcopy比较绝,内层子对象的id直接就是不一样的,这样今后对子对象咋修改都和原对象没一毛钱关系了。

终极结论

(1)直接赋值,除非不可变类型,其它情况一律一个变,另外一个也变。
(2)浅拷贝,除非对可变类型内部的可变子对象进行修改操作外,一律一个变,另一个不变。
(3)深拷贝,使用效果看,一律都不相互影响。

感悟:Python中的赋值因为一律都是“指针”或“引用”的赋值,所以比较“霸道”,这种“霸道”的含义是赋值前后的两个变量除非是不可变类型,否则一律是一个变另外一个跟着变。这种“霸道”体现了Python对资源的“吝啬”,但有时也给编程带来一些麻烦,比如一个列表a,我现在就想对a进行一些增删改,但又不希望破坏a原本的值,这时候就需要Pyhton把a中的值克隆出来一份给列表b,我现在对b的任何操作,a均不受影响。此时就需要引入Copy库或对a进行列表切片后再赋值给b来完成这个愿望。

初学者往往理解不深或受其它语言的影响,可能把浅拷贝和深拷贝对立来看,其实两者是哥们关系,它们的出现都是针对赋值的“霸道”。浅拷贝和深拷贝其实是想把拷贝前后的两个变量之间脱离开,一个变另外一个不变,这是它们哥俩的“初心”。这时有深拷贝就行了,但为啥还有有它弟弟“浅拷贝”呢?估计设计者可能出于节省资源的目的,当在一个可变类型变量内部含有子对象的时候,做了一下折中的变化,使浅拷贝后外层的id不一样,但内层的id一致。

额外注意: a = b 一对一直接赋值后,如果对其中一个做了某些特殊的修改,或使用类似 += 赋值时要注意以下情况:

#a虽然是可变类型,但赋值给b后对a做修改,b不随之变换的几种特例:
a = [1,2,3,4]
c = [4,5,6,7]
b = a
a = a + c
print(a) #打印[1, 2, 3, 4, 4, 5, 6, 7]
print(b)  #打印[1, 2, 3, 4]

#上例如果改为+=赋值,则b会随之变化,+= 等价调用了extend方法
a = [1,2,3,4]
c = [4,5,6,7]
b = a
a += c
print(b)  #[1, 2, 3, 4, 4, 5, 6, 7]

#再看下面的对比例子
num = [1,2,3,4]
def numf(num):
    num = num + num
numf(num)
print(num)   #打印[1, 2, 3, 4]

num = [1,2,3,4]
def numf(num):
    num += num
numf(num)
print(num)   #打印[1, 2, 3, 4, 1, 2, 3, 4]


#集合的一些操作
a = {1,2,3,4}
c = {3,4,5,6}
b = a
a = a | c #合并两个集合
print(a)  #打印{1, 2, 3, 4, 5, 6}
print(b)  #打印{1, 2, 3, 4}

练习题

#(1)试写出下列程序的运行结果:
a = [[1,2,3],[4,5,6]]
d = a
del d[0]
print(a)
#(2)试写出下列程序的运行结果:
a = '哈尔滨商业大学'
d = a
d = d + 'Harbin University of Commerce'
print(a)
#(3)试写出下列程序的运行结果:
import copy
a = [1,2,3,4,5]
b = copy.copy(a)
b.append(6)
print(a)
#(4)试写出下列程序的运行结果:
a = [123,'hrb',[1,2,3]]
b = copy.copy(a)
a.append([4,5,6])
print(b)
#(5)试写出下列程序的运行结果:
a = [123,'hrb',[1,2,3]]
b = copy.copy(a)
a[2].append(4)
print(b)
#(6)试写出下列程序的运行结果:
a = [123,'hrb',[1,2,3]]
b = copy.deepcopy(a)
a[2].append(4)
print(b)
#(7)试写出下列程序的运行结果:
a = [123,'hrb',[1,2,3]]
b = copy.copy(a)
b[2] = 'hello'
print(a)
#(8)试写出下列程序的运行结果:
a = [123,'hrb',[1,2,3]]
b = copy.copy(a)
print(id(a[0]) == id(b[0]))
#(9)试写出下列程序的运行结果:
a = [123,'hrb',[1,2,3]]
b = copy.deepcopy(a)
print(id(a[0]) == id(b[0]))
#(10)试写出下列程序的运行结果:
a = [123,'hrb',[1,2,3]]
b = copy.deepcopy(a)
print(id(a[2]) == id(b[2]))

参考答案
(1)[[4, 5, 6]]
(2)哈尔滨商业大学
(3) [1, 2, 3, 4, 5]
(4)[123, ‘hrb’, [1, 2, 3]]
(5)[123, ‘hrb’, [1, 2, 3, 4]]
(6)[123, ‘hrb’, [1, 2, 3]]
(7)[123, ‘hrb’, [1, 2, 3]]
(8)True
(9)True
(10)False

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值