- 前言
- 目标读者
- 非目标读者
- 本书的结构
- 以实践为基础
- 硬件
- 杂谈:个人的一点看法
- 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.4 重载标量乘法运算符 *
Vector([1, 2, 3]) * x 是什么意思?如果 x 是数字,就是计算标量积(scalar product),结果是一个新 Vector 实例,各个分量都会乘以 x——这也叫元素级乘法(elementwise multiplication)。
>>> v1 = Vector([1, 2, 3]) >>> v1 * 10 Vector([10.0, 20.0, 30.0]) >>> 11 * v1 Vector([11.0, 22.0, 33.0])
涉及 Vector 操作数的积还有一种,叫两个向量的点积(dot product);如果把一个向量看作 1×N 矩阵,把另一个向量看作 N×1 矩阵,那么就是矩阵乘法。NumPy 等库目前的做法是,不重载这两种意义的 *,只用 * 计算标量积。例如,在 NumPy 中,点积使用 numpy.dot() 函数计算。5
5从 Python 3.5 起,@ 记号可以用作中缀点积运算符。详情参见“Python 3.5 新引入的中缀运算符 @”附注栏。
回到标量积的话题。我们依然先实现最简可用的 __mul__ 和 __rmul__ 方法:
# 在Vector类中定义 def __mul__(self, scalar): return Vector(n * scalar for n in self) def __rmul__(self, scalar): return self * scalar
这两个方法确实可用,但是提供不兼容的操作数时会出问题。scalar 参数的值要是数字,与浮点数相乘得到的积是另一个浮点数(因为 Vector 类在内部使用浮点数数组)。因此,不能使用复数,但可以是 int、bool(int 的子类),甚至 fractions.Fraction 实例等标量。
我们可以像示例 13-10 那样,采用鸭子类型技术,在 __mul__ 方法中捕获 TypeError。但是,这个问题有个更易于理解的方式,而且也更合理:白鹅类型。我们将使用 isinstance() 检查 scalar 的类型,但是不硬编码具体的类型,而是检查 numbers.Real 抽象基类。这个抽象基类涵盖了我们所需的全部类型,而且还支持以后声明为 numbers.Real 抽象基类的真实子类或虚拟子类的数值类型。示例 13-11 展示了白鹅类型的实际运用——显式检查抽象类型。完整的代码清单参见本书的代码仓库。
你可能还记得 11.6 节说过,decimal.Decimal 没有把自己注册为 numbers.Real 的虚拟子类。因此,Vector 类不会处理 decimal.Decimal 数字。
示例 13-11 vector_v7.py:增加 * 运算符方法
from array import array import reprlib import math import functools import operator import itertools import numbers # ➊ class Vector: typecode = 'd' def __init__(self, components): self._components = array(self.typecode, components) # 排版需要,省略了很多方法 # 参见https://github.com/fluentpython/example-code中的vector_v7.py def __mul__(self, scalar): if isinstance(scalar, numbers.Real): # ➋ return Vector(n * scalar for n in self) else: # ➌ return NotImplemented def __rmul__(self, scalar): return self * scalar # ➍
❶ 为了检查类型,导入 numbers 模块。
❷ 如果 scalar 是 numbers.Real 某个子类的实例,用分量的乘积创建一个新 Vector 实例。
❸ 否则,返回 NotImplemented,让 Python 尝试在 scalar 操作数上调用 __rmul__ 方法。
❹ 这里,__rmul__ 方法只需执行 self * scalar,委托给 __mul__ 方法。
有了示例 13-11 中的代码之后,我们可以拿 Vector 实例乘以常规的标量值和不那么寻常的数字类型了:
>>> v1 = Vector([1.0, 2.0, 3.0]) >>> 14 * v1 Vector([14.0, 28.0, 42.0]) >>> v1 * True Vector([1.0, 2.0, 3.0]) >>> from fractions import Fraction >>> v1 * Fraction(1, 3) Vector([0.3333333333333333, 0.6666666666666666, 1.0])
通过实现 + 和 *,我们讲解了编写中缀运算符最常用的模式。+ 和 * 用的技术对表 13-1 中列出的所有运算符都适用(就地运算符在 13.6 节讨论)。
表13-1:中缀运算符方法的名称(就地运算符用于增量赋值;比较运算符在表13-2中)
运算符 | 正向方法 | 反向方法 | 就地方法 | 说明 |
+ | __add__ | __radd__ | __iadd__ | 加法或拼接 |
- | __sub__ | __rsub__ | __isub__ | 减法 |
* | __mul__ | __rmul__ | __imul__ | 乘法或重复复制 |
/ | __truediv__ | __rtruediv__ | __itruediv__ | 除法 |
// | __floordiv__ | __rfloordiv__ | __ifloordiv__ | 整除 |
% | __mod__ | __rmod__ | __imod__ | 取模 |
divmod() | __divmod__ | __rdivmod__ | __idivmod__ | 返回由整除的商和模数组成的元组 |
**,pow() | __pow__ | __rpow__ | __ipow__ | 取幂* |
@ | __matmul__ | __rmatmul__ | __imatmul__ | 矩阵乘法# |
& | __and__ | __rand__ | __iand__ | 位与 |
| | __or__ | __ror__ | __ior__ | 位或 |
^ | __xor__ | __rxor__ | __ixor__ | 位异或 |
<< | __lshift__ | __rlshift__ | __ilshift__ | 按位左移 |
>> | __rshift__ | __rrshift__ | __irshift__ | 按位右移 |
* pow 的第三个参数 modulo 是可选的:pow(a, b, modulo),直接调用特殊方法时也支持这个参数(如 a.__pow__(b, modulo))。
# Python 3.5 新引入的。
众多比较运算符也是一类中缀运算符,但是规则稍有不同。我们将在下一节讨论众多比较运算符。
下述附注栏介绍了 Python 3.5(写作本书时尚未发布 6)引入的 @ 运算符,选读。
6现已发布。——编者注
Python 3.5 新引入的中缀运算符 @
Python 3.4 没有为点积提供中缀运算符。不过,写作本书时,Python 3.5 的 pre-alpha 版实现了“PEP 465 — A dedicated infix operator for matrix multiplication”,提供了点积所需的 @ 记号(例如,a @ b 是 a 和 b 的点积)。@ 运算符由特殊方法 __matmul__、__rmatmul__ 和 __imatmul__ 提供支持,名称取自“matrix multiplication”(矩阵乘法)。目前,标准库还没用到这些方法,但是 Python 3.5 的解释器能识别,因此 NumPy 团队(以及我们自己)可以在用户定义的类型中支持 @ 运算符。Python 解析器也做了修改,能处理中缀运算符 @(在 Python 3.4 中,a @ b 是一种句法错误)。
为了体验一下,我从源码编译了 Python 3.5,然后为 Vector 实现了点积运算符 @,还做了测试。
下面是我做的最简单的测试:
>>> va = Vector([1, 2, 3]) >>> vz = Vector([5, 6, 7]) >>> va @ vz == 38.0 # 1*5 + 2*6 + 3*7 True >>> [10, 20, 30] @ vz 380.0 >>> va @ 3 Traceback (most recent call last): ... TypeError: unsupported operand type(s) for @: 'Vector' and 'int'
下面是相应特殊方法的代码:
class Vector: # 排版需要,省略了很多方法 def __matmul__(self, other): try: return sum(a * b for a, b in zip(self, other)) except TypeError: return NotImplemented def __rmatmul__(self, other): return self @ other
完整的源码在本书代码仓库里的 vector_py3_5.py 文件中。
记得要在 Python 3.5 中测试,否则会导致 SyntaxError !
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论