复制列表(或多数内置的可变集合)最简单的方式是使用内置的类型构造方法。例如:

>>> l1 = [3, [55, 44], (7, 8, 9)]
>>> l2 = list(l1)  ➊
>>> l2
[3, [55, 44], (7, 8, 9)]
>>> l2 == l1  ➋
True
>>> l2 is l1  ➌
False

list(l1) 创建 l1 的副本。

❷ 副本与源列表相等。

❸ 但是二者指代不同的对象。对列表和其他可变序列来说,还能使用简洁的 l2 = l1[:] 语句创建副本。

然而,构造方法或 [:] 做的是浅复制(即复制了最外层容器,副本中的元素是源容器中元素的引用)。如果所有元素都是不可变的,那么这样没有问题,还能节省内存。但是,如果有可变的元素,可能就会导致意想不到的问题。

在示例 8-6 中,我们为一个包含另一个列表和一个元组的列表做了浅复制,然后做了些修改,看看对引用的对象有什么影响。

 如果你手头有联网的电脑,我强烈建议你在 Python Tutor 网站(http://www.pythontutor.com)中查看示例 8-6 的交互式动画。写作本书时,无法直接链接 pythontutor.com 中准备好的示例,不过这个工具很出色,因此值得花点时间复制粘贴代码。

 

示例 8-6 为一个包含另一个列表的列表做浅复制;把这段代码复制粘贴到 Python Tutor 网站中,看看动画效果

l1 = [3, [66, 55, 44], (7, 8, 9)]
l2 = list(l1)      # ➊
l1.append(100)     # ➋
l1[1].remove(55)   # ➌
print('l1:', l1)
print('l2:', l2)
l2[1] += [33, 22]  # ➍
l2[2] += (10, 11)  # ➎
print('l1:', l1)
print('l2:', l2)

l2l1 的浅复制副本。此时的状态如图 8-3 所示。

{%}

图 8-3:示例 8-6 执行 l2 = list(l1) 赋值后的程序状态。l1l2 指代不同的列表,但是二者引用同一个列表 [66, 55, 44] 和元组 (7, 8, 9)(图表由 Python Tutor 网站生成)

❷ 把 100 追加到 l1 中,对 l2 没有影响。

❸ 把内部列表 l1[1] 中的 55 删除。这对 l2 有影响,因为 l2[1] 绑定的列表与 l1[1] 是同一个。

❹ 对可变的对象来说,如 l2[1] 引用的列表,+= 运算符就地修改列表。这次修改在 l1[1] 中也有体现,因为它是 l2[1] 的别名。

❺ 对元组来说,+= 运算符创建一个新元组,然后重新绑定给变量 l2[2]。这等同于 l2[2] = l2[2] + (10, 11)。现在,l1l2 中最后位置上的元组不是同一个对象。如图 8-4 所示。

示例 8-6 的输出在示例 8-7 中,对象的最终状态如图 8-4 所示。

示例 8-7 示例 8-6 的输出

l1: [3, [66, 44], (7, 8, 9), 100]
l2: [3, [66, 44], (7, 8, 9)]
l1: [3, [66, 44, 33, 22], (7, 8, 9), 100]
l2: [3, [66, 44, 33, 22], (7, 8, 9, 10, 11)]

{%}

图 8-4:l1l2 的最终状态:二者依然引用同一个列表对象,现在列表的值是 [66, 44, 33, 22],不过 l2[2] += (10, 11) 创建一个新元组,内容是 (7, 8, 9, 10, 11),它与 l1[2] 引用的元组 (7, 8, 9) 无关(图表由 Python Tutor 网站生成)

现在你应该明白了,浅复制容易操作,但是得到的结果可能并不是你想要的。接下来说明如何做深复制。

浅复制没什么问题,但有时我们需要的是深复制(即副本不共享内部对象的引用)。copy 模块提供的 deepcopycopy 函数能为任意对象做深复制和浅复制。

为了演示 copy()deepcopy() 的用法,示例 8-8 定义了一个简单的类,Bus。这个类表示运载乘客的校车,在途中乘客会上车或下车。

示例 8-8 校车乘客在途中上车和下车

class Bus:

    def __init__(self, passengers=None):
        if passengers is None:
            self.passengers = []
        else:
            self.passengers = list(passengers)

    def pick(self, name):
        self.passengers.append(name)

    def drop(self, name):
        self.passengers.remove(name)

接下来,在示例 8-9 中的交互式控制台中,我们将创建一个 Bus 实例(bus1)和两个副本,一个是浅复制副本(bus2),另一个是深复制副本(bus3),看看在 bus1 有学生下车后会发生什么。

示例 8-9 使用 copydeepcopy 产生的影响

>>> import copy
>>> bus1 = Bus(['Alice', 'Bill', 'Claire', 'David'])
>>> bus2 = copy.copy(bus1)
>>> bus3 = copy.deepcopy(bus1)
>>> id(bus1), id(bus2), id(bus3)
(4301498296, 4301499416, 4301499752)  ➊
>>> bus1.drop('Bill')
>>> bus2.passengers
['Alice', 'Claire', 'David']          ➋
>>> id(bus1.passengers), id(bus2.passengers), id(bus3.passengers)
(4302658568, 4302658568, 4302657800)  ➌
>>> bus3.passengers
['Alice', 'Bill', 'Claire', 'David']  ➍

❶ 使用 copydeepcopy,创建 3 个不同的 Bus 实例。

bus1 中的 'Bill' 下车后,bus2 中也没有他了。

❸ 审查 passengers 属性后发现,bus1bus2 共享同一个列表对象,因为 bus2bus1 的浅复制副本。

bus3bus1 的深复制副本,因此它的 passengers 属性指代另一个列表。

注意,一般来说,深复制不是件简单的事。如果对象有循环引用,那么这个朴素的算法会进入无限循环。deepcopy 函数会记住已经复制的对象,因此能优雅地处理循环引用,如示例 8-10 所示。

示例 8-10 循环引用:b 引用 a,然后追加到 a 中;deepcopy 会想办法复制 a

>>> a = [10, 20]
>>> b = [a, 30]
>>> a.append(b)
>>> a
[10, 20, [[...], 30]]
>>> from copy import deepcopy
>>> c = deepcopy(a)
>>> c
[10, 20, [[...], 30]]

此外,深复制有时可能太深了。例如,对象可能会引用不该复制的外部资源或单例值。我们可以实现特殊方法 __copy__()__deepcopy__(),控制 copydeepcopy 的行为,详情参见 copy 模块的文档(http://docs.python.org/3/library/copy.html)。

通过别名共享对象还能解释 Python 中传递参数的方式,以及使用可变类型作为参数默认值引起的问题。接下来讨论这些问题。