Skip to content

Latest commit

 

History

History
165 lines (124 loc) · 5.06 KB

basic8.md

File metadata and controls

165 lines (124 loc) · 5.06 KB

Python的基础故事(八)——浅深拷贝

本文为大家总结一下Python中的浅拷贝与深拷贝问题。

Shallow vs Deep

所谓拷贝,即复制一个同样的对象;所谓浅拷贝,即复制的对象结构上与原对象一致,数据上完全是原对象数据的引用;而所谓深拷贝,即新对象的数据也从原对象数据中拷贝出了新的一份独立的数据。

如果对此还不熟悉,可以看下面的例子。我们知道Python中的标识符是对对象的引用,例如,a = 3表示在内存中某一个位置存储了数字3,而a表示对这一内存地址的引用;b = [1, 2, 3]表示在内存的三个位置,存放了三个数字123,这三个数字的地址的引用组成了一个列表,而这个列表则由标识符b引用着。浅拷贝与深拷贝区分的目标就是存在可变对象的一些混合对象,例如c = [1, 2, [1, 2, 3]]。这里c[2]存储的是对列表[1, 2, 3]引用。对于c而言,若采用浅拷贝,则新对象的第三个元素依旧是同一个列表[1, 2, 3]的引用,修改原始列表会导致新对象中的列表也被修改(因为是同一个列表);而深拷贝时,新对象的第三个元素则被复制为全新的[1, 2, 3],与原始列表毫无关联。表示起来如下:

c = [1, 2, [1, 2, 3]]
# 浅拷贝
d = shallow_copy(c)

c[2][0] = 100
print(d) # [1, 2, [100, 2, 3]]

c = [1, 2, [1, 2, 3]]
# 深拷贝
e = deep_copy(c)
c[2][0] = 100
print(e) # [1, 2, [1, 2, 3]]

容器类型的拷贝

列表

我们知道,通过切片可以复制一个新列表。

a = [1, 2, 3]
b = a[:]
a[0] = 4
print(b)
[1, 2, 3]

那么,列表切片属于浅拷贝还是深拷贝呢?我们来试一下:

a = [1, 2, 3]
b = [4, 5, a]
c = b[:]

原来列表切片实际上也是浅拷贝。

字典

字典项存在copy方法用于拷贝另一个字典项,以及update方法用于更新另一个字典项的内容到当前字典中,或是通过dict从另一个字典创建,我们分别测试一下:

a = {
    'key1': 'v1',
    'key2': [1, 2, 3],
}
b = a.copy()
c = {}
c.update(a)
d = dict(a)

a['key2'][0] = 10
print(b)
{'key1': 'v1', 'key2': [10, 2, 3]}

print(c)
{'key1': 'v1', 'key2': [10, 2, 3]}

print(d)
{'key1': 'v1', 'key2': [10, 2, 3]}

可以看到,无论是copy还是update还是直接创建,都是浅拷贝。

深拷贝

如何进行深拷贝呢?利用标准库copy中的deepcopy函数:

import copy
a = [1, 2, 3]
b = [4, 5, a]
c = copy.deepcopy(b)
a[0] = 10
print(c)

a = {
    'key1': 'v1',
    'key2': [1, 2, 3],
}
b = copy.deepcopy(a)

a['key2'][0] = 10
print(b)

自定义对象拷贝

两个特殊方法控制着自定义对象的浅拷贝与深拷贝,即__copy____deepcopy__。我们可以通过实现这两个方法来自定义拷贝过程:

class A:
    def __init__(self):
        self.a = [1, 2, [1, 2, 3]]

    @classmethod
    def _dcopy(cls, x):
        typ = type(x)
        return cls._dispatch_table.get(typ)(x)

    @classmethod
    def _copylist(cls, x):
        y = []
        for i in x:
            y.append(cls._dcopy(i))
        return y

    def __copy__(self):
        print('shallow copy')
        cls = self.__class__
        obj = cls()
        obj.a = self.a[:]
        return obj

    def __deepcopy__(self, memo):
        print('deep copy')
        cls = self.__class__
        obj = cls()
        obj.a = self._dcopy(self.a)
        return obj

A._dispatch_table = {
    list: A._copylist,
    int: lambda x: x,
}

这里我们分别定义了浅拷贝与深拷贝来拷贝A对象,针对的是A中的列表属性a。我们先来看一下效果:

import copy
a1 = A()
a2 = copy.copy(a1)
print(a2.a)
# shallow copy
# [1, 2, [1, 2, 3]]

a3 = copy.deepcopy(a1)
print(a3.a)
# deep copy
# [1, 2, [1, 2, 3]]

a1.a[2][0] = 100
print(a2.a)
[1, 2, [100, 2, 3]]

print(a3.a)
[1, 2, [1, 2, 3]]

可以看到,copy.copycopy.deepcopy分别使用了类内对应的两个特殊方法。__copy__直接生成新的对象并浅拷贝了a属性,这里简单介绍一下__deepcopy__的流程。首先__deepcopy__会调用类方法_dcopy来拷贝属性a_dcopy会查询拷贝目标的类型,并在类属性_dispatch_table中获得类型所对应的拷贝方法。这里为了简单起见,仅仅定义了listint类型对应的方法。因为int不可变,所以拷贝int对象直接返回自身即可;而对于list对象,我们则调用类方法_copylist来拷贝。_copylist遍历目标列表,对每个元素再递归调用_dcopy来进行拷贝,最后放进一个新的列表中。这样,针对嵌套列表,_copylist也能够实现深拷贝。

实际上,这一小段流程正是标准库copydeepcopy函数的基本流程,只不过deepcopy包含了所有基础类型的拷贝方法,并保证了正确的对象引用计数,还兼顾了自定义类型中的一些特殊方法。具体程序可以查看Python源码中的copy.py模块实现。