先来看一段 Python 代码
1 | a = dict() |
我们的本意是想得到如下的结果:
1 | {'k_1': {'a': 1}, 'k_0': {'a': 0}, 'k_3': {'a': 3}, 'k_2': {'a': 2}, 'k_4': {'a': 4}} |
但是上述代码的执行结果却是:
1 | {'k_1': {'a': 4}, 'k_0': {'a': 4}, 'k_3': {'a': 4}, 'k_2': {'a': 4}, 'k_4': {'a': 4}} |
Python 当中,一切皆对象。Python 中的对象可以分为两类,一类是可变对象,如list
, dict
等,另一类是不可变对象,如string
, int
, float
等。
1 | 'hello world' a = |
不可变对象不是值不可变,当要改变一个变量的值的时候,会在内存中开辟一段新的空间存入新值,然后变量指向这个新值。可变对象改变值不会开辟新的内存空间。
当给一个对象进行赋值时,Python 会使用一个指向原对象的引用。1
2
3
4
5
6
7
8
91, 2, 3] a = [
b = a
4) a.append(
b
[1, 2, 3, 4]
id(a)
4574920288
id(b)
4574920288
list [1, 2, 3]
在内存中开辟了一段空间,变量a
指向了这段空间,当将a
赋值给b
的时候,b
也指向了这段空间。
Python 从来不对赋值操作进行“隐式”的拷贝。如果想要复制出一个新的对象,对它进行修改操作又不影响原来的对象,那么必须“显式”的做一个拷贝。
Python 的copy
模块提供了copy
和deepcopy
两个函数来创建拷贝。这两个函数的区别是:
copy
函数可以拷贝出一个新的对象,但是这个新对象的内容和属性还是会引用原对象的内容和属性。deepcopy
函数可以做到对一个对象的完整拷贝,拷贝出来的新对象,其内部的所有内容都会重新开辟出一段新的内存空间。
比如:
1 | # 浅拷贝 |
如果需要拷贝一些容器对象,还必须递归地拷贝其内部引用的对象(包括所有的元素、属性、元素的元素、元素的属性等),使用copy.deepcopy
这种深拷贝操作,会消耗相当的时间和内存,但如果确实需要这样的效果,那么深拷贝操作将是唯一的选择。
当在记录这篇笔记的时候,我想使用如下的例子:
1 | # 浅拷贝 |
从上述例子中可以看出,深拷贝并没有达到我们想要的效果。所以这是为什么呢?这个与 Python 的类属性和实例属性有关。从《关于 Python 类属性与实例属性的讨论》这篇文章中我们可以知道,当实例对象没有某个属性的时候,它会向上查找,也就是会到类对象中查找。上面的例子c2 = copy.deepcopy(c1)
当执行这条语句时,实例c1
并没有实例对象属性,它会查找类对象aClass的属性,所以给类对象aClass的属性a
添加了一个元素,因为c2
也没有实例对象属性,所以c2.a
也从aClass
中获得。c1.a
与c2.a
是对同一内存空间的相同引用。
对于浅拷贝来说,还有其他的方法能够实现同样的效果。比如对于列表L
,调用list(L)
,对于字典d
,调用dict(d)
。所以我们可以得到通用的办法:当拷贝的这个对象o
,属于Python内建的类型t
,可以用t(o)
来进行拷贝,如对于列表a,可以用list(a)
来拷贝。
我们再回到开头举的那个例子。这个例子中b
是可变对象dict
,每次的循环a['k_0']
,a['k_1']
…与d
都是同一块内存空间的相同引用。所以当d
改变时候,其余的变量也跟着改变了。