- 前言
- 目标读者
- 非目标读者
- 本书的结构
- 以实践为基础
- 硬件
- 杂谈:个人的一点看法
- 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 术语表
- 作者简介
- 关于封面
13.6 增量赋值运算符
Vector 类已经支持增量赋值运算符 += 和 *= 了,如示例 13-15 所示。
示例 13-15 增量赋值不会修改不可变目标,而是新建实例,然后重新绑定
>>> v1 = Vector([1, 2, 3]) >>> v1_alias = v1 # ➊ >>> id(v1) # ➋ 4302860128 >>> v1 += Vector([4, 5, 6]) # ➌ >>> v1 # ➍ Vector([5.0, 7.0, 9.0]) >>> id(v1) # ➎ 4302859904 >>> v1_alias # ➏ Vector([1.0, 2.0, 3.0]) >>> v1 *= 11 # ➐ >>> v1 # ➑ Vector([55.0, 77.0, 99.0]) >>> id(v1) 4302858336
❶ 复制一份,供后面审查 Vector([1, 2, 3]) 对象。
❷ 记住一开始绑定给 v1 的 Vector 实例的 ID。
❸ 增量加法运算。
❹ 结果与预期相符……
❺ ……但是创建了新的 Vector 实例。
❻ 审查 v1_alias,确认原来的 Vector 实例没被修改。
❼ 增量乘法运算。
❽ 同样,结果与预期相符,但是创建了新的 Vector 实例。
如果一个类没有实现表 13-1 列出的就地运算符,增量赋值运算符只是语法糖:a += b 的作用与 a = a + b 完全一样。对不可变类型来说,这是预期的行为,而且,如果定义了 __add__ 方法的话,不用编写额外的代码,+= 就能使用。
然而,如果实现了就地运算符方法,例如 __iadd__,计算 a += b 的结果时会调用就地运算符方法。这种运算符的名称表明,它们会就地修改左操作数,而不会创建新对象作为结果。
不可变类型,如 Vector 类,一定不能实现就地特殊方法。这是明显的事实,不过还是值得提出来。
为了展示如何实现就地运算符,我们将扩展示例 11-12 中的 BingoCage 类,实现 __add__ 和 __iadd__ 方法。
我们把子类命名为 AddableBingoCage。示例 13-16 是我们想让 + 运算符具有的行为。
示例 13-16 使用 + 运算符新建 AddableBingoCage 实例
>>> vowels = 'AEIOU' >>> globe = AddableBingoCage(vowels) ➊ >>> globe.inspect() ('A', 'E', 'I', 'O', 'U') >>> globe.pick() in vowels ➋ True >>> len(globe.inspect()) ➌ 4 >>> globe2 = AddableBingoCage('XYZ') ➍ >>> globe3 = globe + globe2 >>> len(globe3.inspect()) ➎ 7 >>> void = globe + [10, 20] ➏ Traceback (most recent call last): ... TypeError: unsupported operand type(s) for +: 'AddableBingoCage' and 'list'
❶ 使用 5 个元素(vowels 中的各个字母)创建一个 globe 实例。
❷ 从中取出一个元素,确认它在 vowels 中。
❸ 确认 globe 的元素数量减少到 4 个了。
❹ 创建第二个实例,它有 3 个元素。
❺ 把前两个实例加在一起,创建第 3 个实例。这个实例有 7 个元素。
❻ AddableBingoCage 实例无法与列表相加,抛出 TypeError。那个错误消息是 __add__ 方法返回 NotImplemented 时 Python 解释器输出的。
AddableBingoCage 是可变的,实现 __iadd__ 方法后的行为如示例 13-17 所示。
示例 13-17 可以使用 += 运算符载入现有的 AddableBingoCage 实例(接续示例 13-16)
>>> globe_orig = globe ➊ >>> len(globe.inspect()) ➋ 4 >>> globe += globe2 ➌ >>> len(globe.inspect()) 7 >>> globe += ['M', 'N'] ➍ >>> len(globe.inspect()) 9 >>> globe is globe_orig ➎ True >>> globe += 1 ➏ Traceback (most recent call last): ... TypeError: right operand in += must be 'AddableBingoCage' or an iterable
❶ 复制一份,供后面检查对象的标识。
❷ 现在 globe 有 4 个元素。
❸ AddableBingoCage 实例可以从同属一类的其他实例那里接受元素。
❹ += 的右操作数也可以是任何可迭代对象。
❺ 在这个示例中,globe 始终指代 globe_orig 对象。
❻ AddableBingoCage 实例不能与非可迭代对象相加,错误消息会指明原因。
注意,与 + 相比,+= 运算符对第二个操作数更宽容。+ 运算符的两个操作数必须是相同类型(这里是 AddableBingoCage),如若不然,结果的类型可能让人摸不着头脑。而 += 的情况更明确,因为就地修改左操作数,所以结果的类型是确定的。
通过观察内置 list 类型的工作方式,我确定了要对 + 和 += 的行为做什么限制。 my_list + x 只能用于把两个列表加到一起,而 my_list += x 可以使用右边可迭代对象 x 中的元素扩展左边的列表。list.extend() 的行为也是这样的,它的参数可以是任何可迭代对象。
我们明确了 AddableBingoCage 的行为,下面来看实现方式,如示例 13-18 所示。
示例 13-18 bingoaddable.py:AddableBingoCage 扩展 BingoCage,支持 + 和 +=
import itertools ➊ from tombola import Tombola from bingo import BingoCage class AddableBingoCage(BingoCage): ➋ def __add__(self, other): if isinstance(other, Tombola): ➌ return AddableBingoCage(self.inspect() + other.inspect()) ➍ else: return NotImplemented def __iadd__(self, other): if isinstance(other, Tombola): other_iterable = other.inspect() ➎ else: try: other_iterable = iter(other) except TypeError: ➏ self_cls = type(self).__name__ msg = "right operand in += must be {!r} or an iterable" raise TypeError(msg.format(self_cls)) self.load(other_iterable) ➐ return self ➑
❶ “PEP 8—Style Guide for Python Code”建议,把导入标准库的语句放在导入自己编写的模块之前。
❷ AddableBingoCage 扩展 BingoCage。
❸ __add__ 方法的第二个操作数只能是 Tombola 实例。
❹ 如果 other 是 Tombola 实例,从中获取元素。
❺ 否则,尝试使用 other 创建迭代器。11
11内置的 iter 函数在下一章讨论。这里,本可以使用 tuple(other),这样做是可以的,但是 .load(...) 方法迭代参数时要构建大量元组,资源消耗大。
❻ 如果尝试失败,抛出异常,并且告知用户该怎么做。如果可能,错误消息应该明确指导用户怎么解决问题。
❼ 如果能执行到这里,把 other_iterable 载入 self。
❽ 重要提醒:增量赋值特殊方法必须返回 self。
通过示例 13-18 中 __add__ 和 __iadd__ 返回结果的方式可以总结出就地运算符的原理。
__add__
调用 AddableBingoCage 构造方法构建一个新实例,作为结果返回。
__iadd__
把修改后的 self 作为结果返回。
最后,示例 13-18 中还有一点要注意:从设计上看,AddableBingoCage 不用定义 __radd__ 方法,因为不需要。如果右操作数是相同类型,那么正向方法 __add__ 会处理,因此,Python 计算 a + b 时,如果 a 是 AddableBingoCage 实例,而 b 不是,那么会返回 NotImplemented,此时或许可以让 b 所属的类接手处理。可是,如果表达式是 b + a,而 b 不是 AddableBingoCage 实例,返回了 NotImplemented,那么 Python 最好放弃,抛出 TypeError,因为无法处理 b。
一般来说,如果中缀运算符的正向方法(如 __mul__)只处理与 self 属于同一类型的操作数,那就无需实现对应的反向方法(如 __rmul__),因为按照定义,反向方法是为了处理类型不同的操作数。
我们对 Python 运算符重载的讨论到此结束。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论