流畅的 Python 第二版(GPT 重译)(六)(4)

简介: 流畅的 Python 第二版(GPT 重译)(六)

流畅的 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)

这适用于Vector2dVector——甚至将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。一旦有一个比较为Falseall就返回False。示例 12-14 展示了使用all__eq__的外观。

示例 12-14. 使用zipall实现的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__angleangles中大量使用生成器表达式,但我们的重点在于提供__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__中特殊逻辑支持的动态属性的名称。

sumanyall涵盖了reduce的最常见用法。请参阅“map、filter 和 reduce 的现代替代品”中的讨论。

⁵ 我们将认真考虑Vector([1, 2]) == (1, 2)这个问题,在“运算符重载 101”中。

⁶ Wolfram Mathworld 网站有一篇关于超球体的文章;在维基百科上,“超球体”重定向到n-球体”条目

⁷ 我为这个演示调整了代码:在 2003 年,reduce是内置的,但在 Python 3 中我们需要导入它;此外,我用my_listsub替换了xy的名称,用于子列表。

相关文章
|
13天前
|
存储 Java 测试技术
流畅的 Python 第二版(GPT 重译)(四)(1)
流畅的 Python 第二版(GPT 重译)(四)
48 1
|
13天前
|
JSON JavaScript Java
流畅的 Python 第二版(GPT 重译)(三)(3)
流畅的 Python 第二版(GPT 重译)(三)
45 5
|
13天前
|
设计模式 算法 程序员
流畅的 Python 第二版(GPT 重译)(五)(3)
流畅的 Python 第二版(GPT 重译)(五)
35 2
|
13天前
|
缓存 算法 Java
流畅的 Python 第二版(GPT 重译)(三)(4)
流畅的 Python 第二版(GPT 重译)(三)
43 4
|
13天前
|
存储 设计模式 缓存
流畅的 Python 第二版(GPT 重译)(五)(2)
流畅的 Python 第二版(GPT 重译)(五)
29 1
|
13天前
|
人工智能 安全 程序员
流畅的 Python 第二版(GPT 重译)(一)(3)
流畅的 Python 第二版(GPT 重译)(一)
13 2
|
13天前
|
Java 测试技术 Go
流畅的 Python 第二版(GPT 重译)(四)(4)
流畅的 Python 第二版(GPT 重译)(四)
22 1
|
13天前
|
消息中间件 缓存 应用服务中间件
流畅的 Python 第二版(GPT 重译)(十)(4)
流畅的 Python 第二版(GPT 重译)(十)
38 0
|
13天前
|
安全 Java 程序员
流畅的 Python 第二版(GPT 重译)(六)(3)
流畅的 Python 第二版(GPT 重译)(六)
9 1
|
13天前
|
存储 Python
流畅的 Python 第二版(GPT 重译)(十)(2)
流畅的 Python 第二版(GPT 重译)(十)
21 0

热门文章

最新文章