*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中)
运算符 |
正向方法 |
反向方法 |
就地方法 |
说明 |
|---|---|---|---|---|
|
|
|
|
加法或拼接 |
|
|
|
|
减法 |
|
|
|
|
乘法或重复复制 |
|
|
|
|
除法 |
|
|
|
|
整除 |
|
|
|
|
取模 |
|
|
|
|
返回由整除的商和模数组成的元组 |
|
|
|
|
取幂* |
|
|
|
|
矩阵乘法# |
|
|
|
|
位与 |
|
|
|
|
位或 |
|
|
|
|
位异或 |
|
|
|
|
按位左移 |
|
|
|
|
按位右移 |
* 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”(https://www.python.org/dev/peps/pep-0465/),提供了点积所需的
@记号(例如,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完整的源码在本书代码仓库(https://github.com/fluentpython/example-code)里的 vector_py3_5.py 文件中。
记得要在 Python 3.5 中测试,否则会导致
SyntaxError!