返回介绍

8.3 默认做浅复制

发布于 2024-02-05 21:59:47 字数 4686 浏览 0 评论 0 收藏 0

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

>>> 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 网站中查看示例 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) 赋值后的程序状态。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)。现在,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:l1l2 的最终状态:二者依然引用同一个列表对象,现在列表的值是 [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 模块的文档

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文