目录
- 浅拷贝与深拷贝总览
- 不可变类型的“拷贝”
- 可变类型的直接赋值
- 可变类型的深拷贝与浅拷贝
- 可变类型的浅拷贝
- 可变类型的深拷贝
- 元组的深浅拷贝
- 附:字典的深浅拷贝
- 转载资料
- 拷贝的使用处
浅拷贝与深拷贝总览
Python 中对象的赋值是直接通过传递引用进行的。需要进行拷贝则需使用标准库中的 copy 模块。
- 直接赋值:直接传递内存地址
- 浅拷贝
copy.copy()
:创建新对象,里面复制所有元素的内存地址 - 深拷贝
copy.deepcopy()
:创建新对象,不可变的复制内存地址,含可变的就递归创建新对象并复制内容(“含可变”表示可变类型以及元组内含可变类型元素,见下方)
- 在深拷贝这里,不会因为元组是不可变类型而不检测(实际上判断的依据应该是是否为容器类型,但是为了方便记忆,我们可以记可变、不可变),而会进一步检测元组里面是否包含可变类型的元素而进行复制。如果元组里有可变元素,则会复制出新的元组!
import copy
a = (1, 2)
print(id(a) == id(copy.deepcopy(a))) # True
a = (1, [2])
print(id(a) == id(copy.deepcopy(a))) # False
- 为什么不可变的是复制内存地址呢🤔?看下小节[不可变类型的“拷贝”]就明白了🧐。
- 不可变类型:Number(数字)、String(字符串)、Tuple(元组)【除元组外都是非容器类型】
- 可变类型:List(列表)、Dictionary(字典)、Set(集合)【都是容器类型】
- 非容器类型:Number(数字)、String(字符串)【都是不可变类型】
- 容器类型:List(列表)、Dictionary(字典)、Tuple(元组)【除元组外都是可变类型】
不可变类型的“拷贝”
不可变类型除元组外都是非容器类型,它们没有拷贝一说(详见下方)。使用 copy 模块进行拷贝的话都是直接赋值,传递内存地址。
- 理解 copy 模块的一些设计原因,首先要了解这个直接赋值而实现“拷贝”的原因。
- 因为拷贝的需求就是希望两个变量互不干扰,要新的一份。而不可变类型的“修改”就已经实现了两者的独立性(它的“修改”都是重新创建一个变量,将新变量指向新对象,旧变量依旧指向旧对象)。因此拷贝内容也就没必要再复制开辟新空间,新旧变量均指向同一个地址即可。
- 图中的验证:
a = "a"
b = a # 直接赋值,进行“拷贝”
print(b is a)
b = "b" # “修改”又是直接赋值了,因为它们无法修改
print(a)
print(b)
print(b is a)
True
a
b
False
- 不可变类型的元组也是直接赋值。但是含有可变元素的元组特殊,它的拷贝就必须考虑到内部可变元素,详见下一小节。
---> 所以在下面的可变类型/容器类型的拷贝中,有关不可变类型的子元素,就直接赋值传递内存地址即可。
要注意的是,这里直接赋值是将新旧两个变量标签直接指向同一个对象。“修改”是针对一个变量标签“修改”,没有动到另一个变量标签。从而两者独立不影响。
可变类型的直接赋值
上面笔者讲了不可变类型的直接赋值,这里来偏个题看看可变类型的直接赋值。这个就是变量标签指向同一个对象。一直都是操作同一个对象,所以显而易见没有达到拷贝的需求:
(图片下面的小标题有误,不想编辑了,意思懂了就行,容器类型里的元组是比较特殊的)
可变类型的深拷贝与浅拷贝
它的深浅拷贝首先均会【创建一个新对象】,但是直接子元素的指向有所不同。这里是将新旧两个变量标签指向了不同的两个对象!(元组特殊,后面注意)
- 所以日后对这两个变量的修改是互不干扰的!这也可以从上面的“不可变类型的直接赋值”图解简易类推:变量 AB 替换成对象 AB(它们一一对应嘛);这两个对象是容器,指向很多“字符串”。
import copy
aList = ["你", "好", 1, [1, 2, 3]]
bList = copy.copy(aList) # 浅拷贝:拷贝了所有元素的内存地址到一个新list对象里
cList = copy.deepcopy(aList) # 深拷贝:在浅拷贝的基础上,还递归为容器元素创建新对象拷贝内容
print(id(aList[3])) # 2249103153728
print(id(bList[3])) # 2249103153728
print(id(cList[3])) # 2249103153216
# 可以看到,浅拷贝里,都是相同的内存地址;而深拷贝会将可变类型子元素再次拷贝!
在直接赋值里,我们以新旧两个变量标签为基准,来看变量内容的修改。
而这里,我们继续看变量标签,它指向了新旧两个容器对象。当容器对象的元素为不可变类型时,遵循上述的逻辑,新建对象赋予容器对象的元素新的地址从而互不影响。
可变类型的浅拷贝
可变类型的浅拷贝中,因为不可变类型 "你"
、"好"
、1
修改都是新建对象赋予新地址,从而两者互不干扰。
但是修改子列表 [1, 2, 3]
不会创建新对象,因此两者这个一直都是引用同一个,互有影响。
- 字典中的浅拷贝:创建两个字典对象,对键创建了新的对象*,但是复制了值的内存地址,它们值是共享的(需要了解字典的内存示意图)。如果值有可变类型,那么依旧互有影响。
- 对键创建了新的对象:例如有字典 A、B:B 是 A 的浅拷贝,此时如果删除 B 的一个键,那么 A 中对应的键是没有影响的。实现了两个字典的这种初步独立,实现了这种浅拷贝的需求。
- 详细图解见[附:字典的深浅拷贝]。
可变类型的深拷贝
可变类型的深拷贝中,进行了递归,为容器元素创建了新对象拷贝内容。从而实现两个变量永远互不干扰。
- 字典中的深拷贝:对键、值都创建了新的对象(不可变类型的依旧只是复制内存地址),两者互不干扰。
元组的深浅拷贝
- 元组是不可变类型,应该是不会进行复制新对象的,是传递内存地址。
- 浅拷贝:元组的浅拷贝实际上是直接传递内存地址。
- 深拷贝:特殊的是含可变类型元素的元组的深拷贝会进行复制:新建一个元组对象,并且递归为可变元素复制创建新对象。
只有这样,深拷贝出来的元组对象与原元组对象才能互相独立。
附:字典的深浅拷贝
理解了上面图解列表的深浅拷贝后,再类推理解字典的深浅拷贝也就不难了。
- 注意到这里面,无论深浅拷贝均复制了键对象哦!
有关上述的测试:略。
转载资料
理解上文所说的后,对照看一下这篇文章里的几张图,看看与自己想的是不是一样的。
- 不可变元组 和 可变列表的深拷贝,浅拷贝区别
这里,第一部分,全为不可变子元素的元组是直接赋值,所以都是 True
;
第二部分,拷贝可变类型,浅拷贝和深拷贝它们的内容是一致的所以 ==
判断时为 True
;而它们拷贝都是新建对象,所以内存地址 ==
判断为 False
。
如果你不能自己看懂此题,说明你还没有真正的理解本文所说的,建议复读几遍。
拷贝的使用处
Python 如果使用到复制的,一般默认使用的是浅拷贝。以下例子等待继续学习补充。
- 函数的参数传递是传递地址,没有拷贝!
比如:切片的 [:]
使用的是浅拷贝、