导航
导航

Python 浅拷贝和深拷贝笔记

先来看一段 Python 代码

1
2
3
4
5
6
7
8
a = dict()
b = dict()

for i in range(0, 5):
b['a'] = i
a['k_%s' % (i)] = b

print a

我们的本意是想得到如下的结果:

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> a = 'hello world'
>>> id(a)
4585762704
>>> a = 'hello fucking world'
>>> id(a)
4582482584

>>> b = [1, 2, 3]
>>> id(b)
4577100792
>>> b.append(4)
>>> b
[1, 2, 3, 4]
>>> id(b)
4577100792

不可变对象不是值不可变,当要改变一个变量的值的时候,会在内存中开辟一段新的空间存入新值,然后变量指向这个新值。可变对象改变值不会开辟新的内存空间。

当给一个对象进行赋值时,Python 会使用一个指向原对象的引用。

1
2
3
4
5
6
7
8
9
>>> a = [1, 2, 3]
>>> b = a
>>> a.append(4)
>>> b
[1, 2, 3, 4]
>>> id(a)
4574920288
>>> id(b)
4574920288

list [1, 2, 3]在内存中开辟了一段空间,变量a指向了这段空间,当将a赋值给b的时候,b也指向了这段空间。

Python 从来不对赋值操作进行“隐式”的拷贝。如果想要复制出一个新的对象,对它进行修改操作又不影响原来的对象,那么必须“显式”的做一个拷贝。

Python 的copy模块提供了copydeepcopy两个函数来创建拷贝。这两个函数的区别是:

  • copy函数可以拷贝出一个新的对象,但是这个新对象的内容和属性还是会引用原对象的内容和属性。
  • deepcopy函数可以做到对一个对象的完整拷贝,拷贝出来的新对象,其内部的所有内容都会重新开辟出一段新的内存空间。

比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 浅拷贝
>>> class aClass(object):
... def __init__(self):
... self.a = [1, 2, 3]
... self.b = 1
>>> c1 = aClass()
>>> c2 = copy.copy(c1)
>>> c1.a.append(4)
>>> c2.a
[1, 2, 3, 4]
>>> c1 is c2
False
>>> c1.a is c2.a
True

# 深拷贝
>>> class aClass(object):
... def __init__(self):
... self.a = [1, 2, 3]
... self.b = 1
>>> c1 = aClass()
>>> c2 = copy.deepcopy(c1)
>>> c1.a.append(4)
>>> c2.a
[1, 2, 3]
>>> c1 is c2
False
>>> c1.a is c2.a
False

如果需要拷贝一些容器对象,还必须递归地拷贝其内部引用的对象(包括所有的元素、属性、元素的元素、元素的属性等),使用copy.deepcopy这种深拷贝操作,会消耗相当的时间和内存,但如果确实需要这样的效果,那么深拷贝操作将是唯一的选择。

当在记录这篇笔记的时候,我想使用如下的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 浅拷贝
>>> class aClass(object):
a = [1, 2, 3]
b = 1
>>> c1 = aClass()
>>> c2 = copy.copy(c1)
>>> c1.a.append(4)
>>> c2.a
[1, 2, 3, 4]
>>> c1 is c2
False
>>> c1.a is c2.a
True

# 深拷贝
>>> class aClass(object):
a = [1, 2, 3]
b = 1
>>> c1 = aClass()
>>> c2 = copy.deepcopy(c1)
>>> c1.a.append(4)
>>> c2.a
[1, 2, 3, 4]
>>> c1 is c2
False
>>> c1.a is c2.a
True

从上述例子中可以看出,深拷贝并没有达到我们想要的效果。所以这是为什么呢?这个与 Python 的类属性和实例属性有关。从《关于 Python 类属性与实例属性的讨论》这篇文章中我们可以知道,当实例对象没有某个属性的时候,它会向上查找,也就是会到类对象中查找。上面的例子c2 = copy.deepcopy(c1)当执行这条语句时,实例c1并没有实例对象属性,它会查找类对象aClass的属性,所以给类对象aClass属性a添加了一个元素,因为c2也没有实例对象属性,所以c2.a也从aClass中获得。c1.ac2.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改变时候,其余的变量也跟着改变了。

支持一下
请 xdd1874 喝一杯咖啡?
  • 微信扫一扫