- 前言
- 目标读者
- 非目标读者
- 本书的结构
- 以实践为基础
- 硬件
- 杂谈:个人的一点看法
- Python 术语表
- Python 版本表
- 排版约定
- 使用代码示例
- 第一部分 序幕
- 第 1 章 Python 数据模型
- 第二部分 数据结构
- 第 2 章 序列构成的数组
- 第 3 章 字典和集合
- 第 4 章 文本和字节序列
- 第三部分 把函数视作对象
- 第 5 章 一等函数
- 第 6 章 使用一等函数实现设计模式
- 第 7 章 函数装饰器和闭包
- 第四部分 面向对象惯用法
- 第 8 章 对象引用、可变性和垃圾回收
- 第 9 章 符合 Python 风格的对象
- 第 10 章 序列的修改、散列和切片
- 第 11 章 接口:从协议到抽象基类
- 第 12 章 继承的优缺点
- 第 13 章 正确重载运算符
- 第五部分 控制流程
- 第 14 章 可迭代的对象、迭代器和生成器
- 14.1 Sentence 类第1版:单词序列
- 14.2 可迭代的对象与迭代器的对比
- 14.3 Sentence 类第2版:典型的迭代器
- 14.4 Sentence 类第3版:生成器函数
- 14.5 Sentence 类第4版:惰性实现
- 14.6 Sentence 类第5版:生成器表达式
- 14.7 何时使用生成器表达式
- 14.8 另一个示例:等差数列生成器
- 14.9 标准库中的生成器函数
- 14.10 Python 3.3 中新出现的句法:yield from
- 14.11 可迭代的归约函数
- 14.12 深入分析 iter 函数
- 14.13 案例分析:在数据库转换工具中使用生成器
- 14.14 把生成器当成协程
- 14.15 本章小结
- 14.16 延伸阅读
- 第 15 章 上下文管理器和 else 块
- 第 16 章 协程
- 第 17 章 使用期物处理并发
- 第 18 章 使用 asyncio 包处理并发
- 第六部分 元编程
- 第 19 章 动态属性和特性
- 第 20 章 属性描述符
- 第 21 章 类元编程
- 结语
- 延伸阅读
- 附录 A 辅助脚本
- Python 术语表
- 作者简介
- 关于封面
8.3 默认做浅复制
复制列表(或多数内置的可变集合)最简单的方式是使用内置的类型构造方法。例如:
>>> 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) 赋值后的程序状态。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 模块的文档。
通过别名共享对象还能解释 Python 中传递参数的方式,以及使用可变类型作为参数默认值引起的问题。接下来讨论这些问题。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论