流畅的 Python 第二版(GPT 重译)(六)(3)https://developer.aliyun.com/article/1484609
示例 12-12。vector_v4.py 的一部分:从 vector_v3.py 添加两个导入和Vector
类的__hash__
方法
from array import array import reprlib import math import functools # ① import operator # ② class Vector: typecode = 'd' # many lines omitted in book listing... def __eq__(self, other): # ③ return tuple(self) == tuple(other) def __hash__(self): hashes = (hash(x) for x in self._components) # ④ return functools.reduce(operator.xor, hashes, 0) # ⑤ # more lines omitted...
①
导入functools
以使用reduce
。
②
导入operator
以使用xor
。
③
对__eq__
没有更改;我在这里列出它是因为在源代码中保持__eq__
和__hash__
靠近是一个好习惯,因为它们需要一起工作。
④
创建一个生成器表达式,以惰性计算每个组件的哈希值。
⑤
将hashes
传递给reduce
,使用xor
函数计算聚合哈希码;第三个参数0
是初始化器(参见下一个警告)。
警告
使用reduce
时,最好提供第三个参数,reduce(function, iterable, initializer)
,以防止出现此异常:TypeError: reduce() of empty sequence with no initial value
(出色的消息:解释了问题以及如何解决)。initializer
是如果序列为空时返回的值,并且作为减少循环中的第一个参数使用,因此它应该是操作的身份值。例如,对于+
,|
,^
,initializer
应该是0
,但对于*
,&
,它应该是1
。
如示例 12-12 中实现的__hash__
方法是一个完美的 map-reduce 计算示例(图 12-2)。
图 12-2。Map-reduce:将函数应用于每个项目以生成新系列(map),然后计算聚合(reduce)。
映射步骤为每个组件生成一个哈希值,减少步骤使用xor
运算符聚合所有哈希值。使用map
而不是genexp使映射步骤更加可见:
def __hash__(self): hashes = map(hash, self._components) return functools.reduce(operator.xor, hashes)
提示
在 Python 2 中,使用map
的解决方案效率较低,因为map
函数会构建一个包含结果的新list
。但在 Python 3 中,map
是惰性的:它创建一个生成器,按需产生结果,从而节省内存——就像我们在示例 12-8 的__hash__
方法中使用的生成器表达式一样。
当我们谈论减少函数时,我们可以用另一种更便宜的方式来替换我们快速实现的__eq__
,至少对于大向量来说,在处理和内存方面更便宜。正如示例 11-2 中介绍的,我们有这个非常简洁的__eq__
实现:
def __eq__(self, other): return tuple(self) == tuple(other)
这适用于Vector2d
和Vector
——甚至将Vector([1, 2])
视为(1, 2)
相等,这可能是一个问题,但我们暂时忽略这一点。⁵ 但对于可能有数千个组件的Vector
实例来说,这是非常低效的。它构建了两个元组,复制了操作数的整个内容,只是为了使用tuple
类型的__eq__
。对于Vector2d
(只有两个组件),这是一个很好的快捷方式,但对于大型多维向量来说不是。比较一个Vector
和另一个Vector
或可迭代对象的更好方法将是示例 12-13。
示例 12-13。使用for
循环中的zip
实现的Vector.__eq__
方法,用于更高效的比较
def __eq__(self, other): if len(self) != len(other): # ① return False for a, b in zip(self, other): # ② if a != b: # ③ return False return True # ④
①
如果对象的长度不同,则它们不相等。
②
zip
生成一个由每个可迭代参数中的项目组成的元组生成器。如果您对zip
不熟悉,请参阅“了不起的 zip”。在①中,需要进行len
比较,因为zip
在其中一个输入耗尽时会停止生成值而没有警告。
③
一旦两个分量不同,立即返回False
。
④
否则,对象相等。
提示
zip
函数的命名是根据拉链拉链器而来,因为物理设备通过相互锁定来自拉链两侧的牙齿对来工作,这与zip(left, right)
所做的事情是一个很好的视觉类比。与压缩文件无关。
示例 12-13 是高效的,但all
函数可以在一行中产生与for
循环相同的聚合计算:如果操作数中对应分量之间的所有比较都为True
,则结果为True
。一旦有一个比较为False
,all
就返回False
。示例 12-14 展示了使用all
的__eq__
的外观。
示例 12-14. 使用zip
和all
实现的Vector.__eq__
:与示例 12-13 相同的逻辑
def __eq__(self, other): return len(self) == len(other) and all(a == b for a, b in zip(self, other))
请注意,我们首先检查操作数的长度是否相等,因为zip
将停止在最短的操作数处。
示例 12-14 是我们在vector_v4.py中选择的__eq__
的实现。
我们通过将Vector2d
的__format__
方法重新引入到Vector
中来结束本章。
Vector Take #5: Formatting
Vector
的__format__
方法将类似于Vector2d
的方法,但不是提供极坐标的自定义显示,而是使用球坐标——也称为“超球面”坐标,因为现在我们支持n维,而在 4D 及以上的维度中,球体是“超球体”。⁶ 因此,我们将自定义格式后缀从'p'
改为'h'
。
提示
正如我们在“Formatted Displays”中看到的,当扩展格式规范迷你语言时,最好避免重用内置类型支持的格式代码。特别是,我们扩展的迷你语言还使用浮点数格式代码'eEfFgGn%'
的原始含义,因此我们绝对必须避免这些。整数使用'bcdoxXn'
,字符串使用's'
。我选择了'p'
来表示Vector2d
的极坐标。代码'h'
表示超球面坐标是一个不错的选择。
例如,给定 4D 空间中的Vector
对象(len(v) == 4
),'h'
代码将产生类似于的显示,其中
r
是大小(abs(v)
),其余数字是角分量Φ₁,Φ₂,Φ₃。
这里是来自vector_v5.py的 doctests 中 4D 空间中球坐标格式的一些示例(参见示例 12-16):
>>> format(Vector([-1, -1, -1, -1]), 'h') '<2.0, 2.0943951023931957, 2.186276035465284, 3.9269908169872414>' >>> format(Vector([2, 2, 2, 2]), '.3eh') '<4.000e+00, 1.047e+00, 9.553e-01, 7.854e-01>' >>> format(Vector([0, 1, 0, 0]), '0.5fh') '<1.00000, 1.57080, 0.00000, 0.00000>'
在我们可以实现__format__
中所需的微小更改之前,我们需要编写一对支持方法:angle(n)
用于计算一个角坐标(例如,Φ₁),以及angles()
用于返回所有角坐标的可迭代对象。我不会在这里描述数学内容;如果你感兴趣,维基百科的“n-sphere”条目有我用来从Vector
的分量数组中计算球坐标的公式。
示例 12-16 是vector_v5.py的完整清单,汇总了自从“Vector Take #1: Vector2d Compatible”以来我们实现的所有内容,并引入了自定义格式。
示例 12-16. vector_v5.py:包含最终Vector
类的 doctests 和所有代码;标注突出显示了支持__format__
所需的添加内容
""" A multidimensional ``Vector`` class, take 5 A ``Vector`` is built from an iterable of numbers:: >>> Vector([3.1, 4.2]) Vector([3.1, 4.2]) >>> Vector((3, 4, 5)) Vector([3.0, 4.0, 5.0]) >>> Vector(range(10)) Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...]) Tests with two dimensions (same results as ``vector2d_v1.py``):: >>> v1 = Vector([3, 4]) >>> x, y = v1 >>> x, y (3.0, 4.0) >>> v1 Vector([3.0, 4.0]) >>> v1_clone = eval(repr(v1)) >>> v1 == v1_clone True >>> print(v1) (3.0, 4.0) >>> octets = bytes(v1) >>> octets b'd\\x00\\x00\\x00\\x00\\x00\\x00\\x08@\\x00\\x00\\x00\\x00\\x00\\x00\\x10@' >>> abs(v1) 5.0 >>> bool(v1), bool(Vector([0, 0])) (True, False) Test of ``.frombytes()`` class method: >>> v1_clone = Vector.frombytes(bytes(v1)) >>> v1_clone Vector([3.0, 4.0]) >>> v1 == v1_clone True Tests with three dimensions:: >>> v1 = Vector([3, 4, 5]) >>> x, y, z = v1 >>> x, y, z (3.0, 4.0, 5.0) >>> v1 Vector([3.0, 4.0, 5.0]) >>> v1_clone = eval(repr(v1)) >>> v1 == v1_clone True >>> print(v1) (3.0, 4.0, 5.0) >>> abs(v1) # doctest:+ELLIPSIS 7.071067811... >>> bool(v1), bool(Vector([0, 0, 0])) (True, False) Tests with many dimensions:: >>> v7 = Vector(range(7)) >>> v7 Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...]) >>> abs(v7) # doctest:+ELLIPSIS 9.53939201... Test of ``.__bytes__`` and ``.frombytes()`` methods:: >>> v1 = Vector([3, 4, 5]) >>> v1_clone = Vector.frombytes(bytes(v1)) >>> v1_clone Vector([3.0, 4.0, 5.0]) >>> v1 == v1_clone True Tests of sequence behavior:: >>> v1 = Vector([3, 4, 5]) >>> len(v1) 3 >>> v1[0], v1[len(v1)-1], v1[-1] (3.0, 5.0, 5.0) Test of slicing:: >>> v7 = Vector(range(7)) >>> v7[-1] 6.0 >>> v7[1:4] Vector([1.0, 2.0, 3.0]) >>> v7[-1:] Vector([6.0]) >>> v7[1,2] Traceback (most recent call last): ... TypeError: 'tuple' object cannot be interpreted as an integer Tests of dynamic attribute access:: >>> v7 = Vector(range(10)) >>> v7.x 0.0 >>> v7.y, v7.z, v7.t (1.0, 2.0, 3.0) Dynamic attribute lookup failures:: >>> v7.k Traceback (most recent call last): ... AttributeError: 'Vector' object has no attribute 'k' >>> v3 = Vector(range(3)) >>> v3.t Traceback (most recent call last): ... AttributeError: 'Vector' object has no attribute 't' >>> v3.spam Traceback (most recent call last): ... AttributeError: 'Vector' object has no attribute 'spam' Tests of hashing:: >>> v1 = Vector([3, 4]) >>> v2 = Vector([3.1, 4.2]) >>> v3 = Vector([3, 4, 5]) >>> v6 = Vector(range(6)) >>> hash(v1), hash(v3), hash(v6) (7, 2, 1) Most hash codes of non-integers vary from a 32-bit to 64-bit CPython build:: >>> import sys >>> hash(v2) == (384307168202284039 if sys.maxsize > 2**32 else 357915986) True Tests of ``format()`` with Cartesian coordinates in 2D:: >>> v1 = Vector([3, 4]) >>> format(v1) '(3.0, 4.0)' >>> format(v1, '.2f') '(3.00, 4.00)' >>> format(v1, '.3e') '(3.000e+00, 4.000e+00)' Tests of ``format()`` with Cartesian coordinates in 3D and 7D:: >>> v3 = Vector([3, 4, 5]) >>> format(v3) '(3.0, 4.0, 5.0)' >>> format(Vector(range(7))) '(0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0)' Tests of ``format()`` with spherical coordinates in 2D, 3D and 4D:: >>> format(Vector([1, 1]), 'h') # doctest:+ELLIPSIS '<1.414213..., 0.785398...>' >>> format(Vector([1, 1]), '.3eh') '<1.414e+00, 7.854e-01>' >>> format(Vector([1, 1]), '0.5fh') '<1.41421, 0.78540>' >>> format(Vector([1, 1, 1]), 'h') # doctest:+ELLIPSIS '<1.73205..., 0.95531..., 0.78539...>' >>> format(Vector([2, 2, 2]), '.3eh') '<3.464e+00, 9.553e-01, 7.854e-01>' >>> format(Vector([0, 0, 0]), '0.5fh') '<0.00000, 0.00000, 0.00000>' >>> format(Vector([-1, -1, -1, -1]), 'h') # doctest:+ELLIPSIS '<2.0, 2.09439..., 2.18627..., 3.92699...>' >>> format(Vector([2, 2, 2, 2]), '.3eh') '<4.000e+00, 1.047e+00, 9.553e-01, 7.854e-01>' >>> format(Vector([0, 1, 0, 0]), '0.5fh') '<1.00000, 1.57080, 0.00000, 0.00000>' """ from array import array import reprlib import math import functools import operator import itertools # ① class Vector: typecode = 'd' def __init__(self, components): self._components = array(self.typecode, components) def __iter__(self): return iter(self._components) def __repr__(self): components = reprlib.repr(self._components) components = components[components.find('['):-1] return f'Vector({components})' def __str__(self): return str(tuple(self)) def __bytes__(self): return (bytes([ord(self.typecode)]) + bytes(self._components)) def __eq__(self, other): return (len(self) == len(other) and all(a == b for a, b in zip(self, other))) def __hash__(self): hashes = (hash(x) for x in self) return functools.reduce(operator.xor, hashes, 0) def __abs__(self): return math.hypot(*self) def __bool__(self): return bool(abs(self)) def __len__(self): return len(self._components) def __getitem__(self, key): if isinstance(key, slice): cls = type(self) return cls(self._components[key]) index = operator.index(key) return self._components[index] __match_args__ = ('x', 'y', 'z', 't') def __getattr__(self, name): cls = type(self) try: pos = cls.__match_args__.index(name) except ValueError: pos = -1 if 0 <= pos < len(self._components): return self._components[pos] msg = f'{cls.__name__!r} object has no attribute {name!r}' raise AttributeError(msg) def angle(self, n): # ② r = math.hypot(*self[n:]) a = math.atan2(r, self[n-1]) if (n == len(self) - 1) and (self[-1] < 0): return math.pi * 2 - a else: return a def angles(self): # ③ return (self.angle(n) for n in range(1, len(self))) def __format__(self, fmt_spec=''): if fmt_spec.endswith('h'): # hyperspherical coordinates fmt_spec = fmt_spec[:-1] coords = itertools.chain([abs(self)], self.angles()) # ④ outer_fmt = '<{}>' # ⑤ else: coords = self outer_fmt = '({})' # ⑥ components = (format(c, fmt_spec) for c in coords) # ⑦ return outer_fmt.format(', '.join(components)) # ⑧ @classmethod def frombytes(cls, octets): typecode = chr(octets[0]) memv = memoryview(octets[1:]).cast(typecode) return cls(memv)
①
导入itertools
以在__format__
中使用chain
函数。
②
使用从n-sphere article调整的公式计算一个角坐标。
③
创建一个生成器表达式,按需计算所有角坐标。
④
使用itertools.chain
生成genexp,以便无缝迭代幅度和角坐标。
⑤
配置带尖括号的球坐标显示。
⑥
配置带括号的笛卡尔坐标显示。
⑦
创建一个生成器表达式,以便按需格式化每个坐标项。
⑧
将格式化的组件用逗号分隔放在方括号或括号内。
注意
在__format__
、angle
和angles
中大量使用生成器表达式,但我们的重点在于提供__format__
以使Vector
达到与Vector2d
相同的实现水平。当我们在第十七章中讨论生成器时,我们将使用Vector
中的一些代码作为示例,然后详细解释生成器技巧。
这就结束了本章的任务。Vector
类将在第十六章中通过中缀运算符进行增强,但我们在这里的目标是探索编写特殊方法的技术,这些方法在各种集合类中都很有用。
章节总结
本章中的Vector
示例旨在与Vector2d
兼容,除了使用接受单个可迭代参数的不同构造函数签名外,就像内置序列类型所做的那样。Vector
通过仅实现__getitem__
和__len__
就表现得像一个序列,这促使我们讨论协议,即鸭子类型语言中使用的非正式接口。
然后我们看了一下my_seq[a:b:c]
语法在幕后是如何工作的,通过创建一个slice(a, b, c)
对象并将其传递给__getitem__
。有了这个知识,我们使Vector
正确响应切片操作,通过返回新的Vector
实例,就像预期的 Python 序列一样。
下一步是通过诸如my_vec.x
这样的表示法为前几个Vector
组件提供只读访问。我们通过实现__getattr__
来实现这一点。这样做打开了通过编写my_vec.x = 7
来为这些特殊组件赋值的可能性,揭示了一个潜在的错误。我们通过实现__setattr__
来修复这个问题,以禁止向单个字母属性赋值。通常,当你编写__getattr__
时,你需要添加__setattr__
,以避免不一致的行为。
实现__hash__
函数为使用functools.reduce
提供了完美的背景,因为我们需要对所有Vector
组件的哈希值连续应用异或运算符^
,以产生整个Vector
的聚合哈希码。在__hash__
中应用reduce
后,我们使用all
内置的 reduce 函数来创建一个更高效的__eq__
方法。
对Vector
的最后一个增强是通过支持球坐标作为默认笛卡尔坐标的替代来重新实现Vector2d
中的__format__
方法。我们使用了相当多的数学和几个生成器来编写__format__
及其辅助函数,但这些都是实现细节——我们将在第十七章中回到生成器。最后一节的目标是支持自定义格式,从而实现Vector
能够做到与Vector2d
一样的一切,甚至更多。
正如我们在第十一章中所做的那样,这里我们经常研究标准 Python 对象的行为,以模拟它们并为Vector
提供“Pythonic”的外观和感觉。
在第十六章中,我们将在Vector
上实现几个中缀运算符。数学将比这里的angle()
方法简单得多,但探索 Python 中中缀运算符的工作方式是面向对象设计的一课。但在我们开始运算符重载之前,我们将暂时离开单个类的工作,转而关注组织多个类的接口和继承,这是第十三章和第十四章的主题。
进一步阅读
在Vector
示例中涵盖的大多数特殊方法也出现在第十一章的Vector2d
示例中,因此“进一步阅读”中的参考资料在这里都是相关的。
强大的reduce
高阶函数也被称为 fold、accumulate、aggregate、compress 和 inject。更多信息,请参阅维基百科的“Fold (higher-order function)”文章,该文章重点介绍了该高阶函数在递归数据结构的函数式编程中的应用。该文章还包括一张列出了几十种编程语言中类似 fold 函数的表格。
“Python 2.5 中的新功能”简要解释了__index__
,旨在支持__getitem__
方法,正如我们在“一个支持切片的 getitem”中看到的。PEP 357—允许任何对象用于切片详细介绍了从 C 扩展的实现者的角度看它的必要性——Travis Oliphant,NumPy 的主要创造者。Oliphant 对 Python 的许多贡献使其成为一种领先的科学计算语言,从而使其在机器学习应用方面处于领先地位。
¹ iter()
函数在第十七章中有介绍,还有__iter__
方法。
² 属性查找比这更复杂;我们将在第五部分中看到详细内容。现在,这个简化的解释就足够了。
³ 尽管__match_args__
存在于支持 Python 3.10 中的模式匹配,但在之前的 Python 版本中设置这个属性是无害的。在本书的第一版中,我将其命名为shortcut_names
。新名称具有双重作用:支持case
子句中的位置模式,并保存__getattr__
和__setattr__
中特殊逻辑支持的动态属性的名称。
⁴ sum
、any
和all
涵盖了reduce
的最常见用法。请参阅“map、filter 和 reduce 的现代替代品”中的讨论。
⁵ 我们将认真考虑Vector([1, 2]) == (1, 2)
这个问题,在“运算符重载 101”中。
⁶ Wolfram Mathworld 网站有一篇关于超球体的文章;在维基百科上,“超球体”重定向到“n-球体”条目。
⁷ 我为这个演示调整了代码:在 2003 年,reduce
是内置的,但在 Python 3 中我们需要导入它;此外,我用my_list
和sub
替换了x
和y
的名称,用于子列表。