- 前言
- 目标读者
- 非目标读者
- 本书的结构
- 以实践为基础
- 硬件
- 杂谈:个人的一点看法
- 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.4 函数的参数作为引用时
Python 唯一支持的参数传递模式是共享传参(call by sharing)。多数面向对象语言都采用这一模式,包括 Ruby、Smalltalk 和 Java(Java 的引用类型是这样,基本类型按值传参)。
共享传参指函数的各个形式参数获得实参中各个引用的副本。也就是说,函数内部的形参是实参的别名。
这种方案的结果是,函数可能会修改作为参数传入的可变对象,但是无法修改那些对象的标识(即不能把一个对象替换成另一个对象)。示例 8-11 中有个简单的函数,它在参数上调用 += 运算符。分别把数字、列表和元组传给那个函数,实际传入的实参会以不同的方式受到影响。
示例 8-11 函数可能会修改接收到的任何可变对象
>>> def f(a, b): ... a += b ... return a ... >>> x = 1 >>> y = 2 >>> f(x, y) 3 >>> x, y ➊ (1, 2) >>> a = [1, 2] >>> b = [3, 4] >>> f(a, b) [1, 2, 3, 4] >>> a, b ➋ ([1, 2, 3, 4], [3, 4]) >>> t = (10, 20) >>> u = (30, 40) >>> f(t, u) (10, 20, 30, 40) >>> t, u ➌ ((10, 20), (30, 40))
❶ 数字 x 没变。
❷ 列表 a 变了。
❸ 元组 t 没变。
与函数参数相关的另一个问题是使用可变值作为默认值,下一节会讨论。
8.4.1 不要使用可变类型作为参数的默认值
可选参数可以有默认值,这是 Python 函数定义的一个很棒的特性,这样我们的 API 在进化的同时能保证向后兼容。然而,我们应该避免使用可变的对象作为参数的默认值。
下面在示例 8-12 中说明这个问题。我们以示例 8-8 中的 Bus 类为基础定义一个新类, HauntedBus,然后修改 __init__ 方法。这一次,passengers 的默认值不是 None,而是 [],这样就不用像之前那样使用 if 判断了。这个“聪明的举动”会让我们陷入麻烦。
示例 8-12 一个简单的类,说明可变默认值的危险
class HauntedBus: """备受幽灵乘客折磨的校车""" def __init__(self, passengers=[]): ➊ self.passengers = passengers ➋ def pick(self, name): self.passengers.append(name) ➌ def drop(self, name): self.passengers.remove(name)
❶ 如果没传入 passengers 参数,使用默认绑定的列表对象,一开始是空列表。
❷ 这个赋值语句把 self.passengers 变成 passengers 的别名,而没有传入 passengers 参数时,后者又是默认列表的别名。
❸ 在 self.passengers 上调用 .remove() 和 .append() 方法时,修改的其实是默认列表,它是函数对象的一个属性。
HauntedBus 的诡异行为如示例 8-13 所示。
示例 8-13 备受幽灵乘客折磨的校车
>>> bus1 = HauntedBus(['Alice', 'Bill']) >>> bus1.passengers ['Alice', 'Bill'] >>> bus1.pick('Charlie') >>> bus1.drop('Alice') >>> bus1.passengers ➊ ['Bill', 'Charlie'] >>> bus2 = HauntedBus() ➋ >>> bus2.pick('Carrie') >>> bus2.passengers ['Carrie'] >>> bus3 = HauntedBus() ➌ >>> bus3.passengers ➍ ['Carrie'] >>> bus3.pick('Dave') >>> bus2.passengers ➎ ['Carrie', 'Dave'] >>> bus2.passengers is bus3.passengers ➏ True >>> bus1.passengers ➐ ['Bill', 'Charlie']
❶ 目前没什么问题,bus1 没有出现异常。
❷ 一开始,bus2 是空的,因此把默认的空列表赋值给 self.passengers。
❸ bus3 一开始也是空的,因此还是赋值默认的列表。
❹ 但是默认列表不为空!
❺ 登上 bus3 的 Dave 出现在 bus2 中。
❻ 问题是,bus2.passengers 和 bus3.passengers 指代同一个列表。
❼ 但 bus1.passengers 是不同的列表。
问题在于,没有指定初始乘客的 HauntedBus 实例会共享同一个乘客列表。
这种问题很难发现。如示例 8-13 所示,实例化 HauntedBus 时,如果传入乘客,会按预期运作。但是不为 HauntedBus 指定乘客的话,奇怪的事就发生了,这是因为 self.passengers 变成了 passengers 参数默认值的别名。出现这个问题的根源是,默认值在定义函数时计算(通常在加载模块时),因此默认值变成了函数对象的属性。因此,如果默认值是可变对象,而且修改了它的值,那么后续的函数调用都会受到影响。
运行示例 8-13 中的代码之后,可以审查 HauntedBus.__init__ 对象,看看它的 __defaults__ 属性中的那些幽灵学生:
>>> dir(HauntedBus.__init__) # doctest: +ELLIPSIS ['__annotations__', '__call__', ..., '__defaults__', ...] >>> HauntedBus.__init__.__defaults__ (['Carrie', 'Dave'],)
最后,我们可以验证 bus2.passengers 是一个别名,它绑定到 HauntedBus.__init__.__defaults__ 属性的第一个元素上:
>>> HauntedBus.__init__.__defaults__[0] is bus2.passengers True
可变默认值导致的这个问题说明了为什么通常使用 None 作为接收可变值的参数的默认值。在示例 8-8 中,__init__ 方法检查 passengers 参数的值是不是 None,如果是就把一个新的空列表赋值给 self.passengers。下一节会说明,如果 passengers 不是 None,正确的实现会把 passengers 的副本赋值给 self.passengers。下面详解。
8.4.2 防御可变参数
如果定义的函数接收可变参数,应该谨慎考虑调用方是否期望修改传入的参数。
例如,如果函数接收一个字典,而且在处理的过程中要修改它,那么这个副作用要不要体现到函数外部?具体情况具体分析。这其实需要函数的编写者和调用方达成共识。
在本章最后一个校车示例中,TwilightBus 实例与客户共享乘客列表,这会产生意料之外的结果。在分析实现之前,我们先从客户的角度看看 TwilightBus 类是如何工作的。
示例 8-14 从 TwilightBus 下车后,乘客消失了
>>> basketball_team = ['Sue', 'Tina', 'Maya', 'Diana', 'Pat'] ➊ >>> bus = TwilightBus(basketball_team) ➋ >>> bus.drop('Tina') ➌ >>> bus.drop('Pat') >>> basketball_team ➍ ['Sue', 'Maya', 'Diana']
❶ basketball_team 中有 5 个学生的名字。
❷ 使用这队学生实例化 TwilightBus。
❸ 一个学生从 bus 下车了,接着又有一个学生下车了。
❹ 下车的学生从篮球队中消失了!
TwilightBus 违反了设计接口的最佳实践,即“最少惊讶原则”。学生从校车中下车后,她的名字就从篮球队的名单中消失了,这确实让人惊讶。
示例 8-15 是 TwilightBus 的实现,随后解释了出现这个问题的原因。
示例 8-15 一个简单的类,说明接受可变参数的风险
class TwilightBus: """让乘客销声匿迹的校车""" def __init__(self, passengers=None): if passengers is None: self.passengers = [] ➊ else: self.passengers = passengers ➋ def pick(self, name): self.passengers.append(name) def drop(self, name): self.passengers.remove(name) ➌
❶ 这里谨慎处理,当 passengers 为 None 时,创建一个新的空列表。
❷ 然而,这个赋值语句把 self.passengers 变成 passengers 的别名,而后者是传给 __init__ 方法的实参(即示例 8-14 中的 basketball_team)的别名。
❸ 在 self.passengers 上调用 .remove() 和 .append() 方法其实会修改传给构造方法的那个列表。
这里的问题是,校车为传给构造方法的列表创建了别名。正确的做法是,校车自己维护乘客列表。修正的方法很简单:在 __init__ 中,传入 passengers 参数时,应该把参数值的副本赋值给 self.passengers,像示例 8-8 中那样做(8.3 节)。
def __init__(self, passengers=None): if passengers is None: self.passengers = [] else: self.passengers = list(passengers) ➊
➊ 创建 passengers 列表的副本;如果不是列表,就把它转换成列表。
在内部像这样处理乘客列表,就不会影响初始化校车时传入的参数了。此外,这种处理方式还更灵活:现在,传给 passengers 参数的值可以是元组或任何其他可迭代对象,例如 set 对象,甚至数据库查询结果,因为 list 构造方法接受任何可迭代对象。自己创建并管理列表可以确保支持所需的 .remove() 和 .append() 操作,这样 .pick() 和 .drop() 方法才能正常运作。
除非这个方法确实想修改通过参数传入的对象,否则在类中直接把参数赋值给实例变量之前一定要三思,因为这样会为参数对象创建别名。如果不确定,那就创建副本。这样客户会少些麻烦。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论