复制列表(或多数内置的可变集合)最简单的方式是使用内置的类型构造方法。例如:
>>> 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)
❶ l2 是 l1 的浅复制副本。此时的状态如图 8-3 所示。

图 8-3:示例 8-6 执行 l2 = list(l1) 赋值后的程序状态。l1 和 l2 指代不同的列表,但是二者引用同一个列表 [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)。现在,l1 和 l2 中最后位置上的元组不是同一个对象。如图 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:l1 和 l2 的最终状态:二者依然引用同一个列表对象,现在列表的值是 [66, 44, 33, 22],不过 l2[2] += (10, 11) 创建一个新元组,内容是 (7, 8, 9, 10, 11),它与 l1[2] 引用的元组 (7, 8, 9) 无关(图表由 Python Tutor 网站生成)
现在你应该明白了,浅复制容易操作,但是得到的结果可能并不是你想要的。接下来说明如何做深复制。
浅复制没什么问题,但有时我们需要的是深复制(即副本不共享内部对象的引用)。copy 模块提供的 deepcopy 和 copy 函数能为任意对象做深复制和浅复制。
为了演示 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 使用
copy和deepcopy产生的影响
>>> 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'] ➍
❶ 使用 copy 和 deepcopy,创建 3 个不同的 Bus 实例。
❷ bus1 中的 'Bill' 下车后,bus2 中也没有他了。
❸ 审查 passengers 属性后发现,bus1 和 bus2 共享同一个列表对象,因为 bus2 是 bus1 的浅复制副本。
❹ bus3 是 bus1 的深复制副本,因此它的 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__(),控制 copy 和 deepcopy 的行为,详情参见 copy 模块的文档(http://docs.python.org/3/library/copy.html)。
通过别名共享对象还能解释 Python 中传递参数的方式,以及使用可变类型作为参数默认值引起的问题。接下来讨论这些问题。