流畅的 Python 第二版(GPT 重译)(八)(2)https://developer.aliyun.com/article/1484690
示例 15-20. contravariant.py:类型定义和install
函数
from typing import TypeVar, Generic class Refuse: # ① """Any refuse.""" class Biodegradable(Refuse): """Biodegradable refuse.""" class Compostable(Biodegradable): """Compostable refuse.""" T_contra = TypeVar('T_contra', contravariant=True) # ② class TrashCan(Generic[T_contra]): # ③ def put(self, refuse: T_contra) -> None: """Store trash until dumped.""" def deploy(trash_can: TrashCan[Biodegradable]): """Deploy a trash can for biodegradable refuse."""
①
垃圾的类型层次结构:废物
是最一般的类型,可堆肥
是最具体的。
②
T_contra
是逆变类型变量的常规名称。
③
TrashCan
在废物类型上是逆变的。
根据这些定义,以下类型的垃圾桶是可接受的:
bio_can: TrashCan[Biodegradable] = TrashCan() deploy(bio_can) trash_can: TrashCan[Refuse] = TrashCan() deploy(trash_can)
更一般的TrashCan[Refuse]
是可接受的,因为它可以接受任何类型的废物,包括生物降解
。然而,TrashCan[Compostable]
不行,因为它不能接受生物降解
:
compost_can: TrashCan[Compostable] = TrashCan() deploy(compost_can) ## mypy: Argument 1 to "deploy" has ## incompatible type "TrashCan[Compostable]" ## expected "TrashCan[Biodegradable]"
让我们总结一下我们刚刚看到的概念。
变异回顾
变异是一个微妙的属性。以下部分总结了不变、协变和逆变类型的概念,并提供了一些关于它们推理的经验法则。
不变类型
当两个参数化类型之间没有超类型或子类型关系时,泛型类型 L
是不变的,而不管实际参数之间可能存在的关系。换句话说,如果 L
是不变的,那么 L[A]
不是 L[B]
的超类型或子类型。它们在两个方面都不一致。
如前所述,Python 的可变集合默认是不变的。list
类型是一个很好的例子:list[int]
与 list[float]
不一致,反之亦然。
一般来说,如果一个形式类型参数出现在方法参数的类型提示中,并且相同的参数出现在方法返回类型中,那么为了确保在更新和读取集合时的类型安全,该参数必须是不变的。
例如,这是 list
内置的类型提示的一部分typeshed:
class list(MutableSequence[_T], Generic[_T]): @overload def __init__(self) -> None: ... @overload def __init__(self, iterable: Iterable[_T]) -> None: ... # ... lines omitted ... def append(self, __object: _T) -> None: ... def extend(self, __iterable: Iterable[_T]) -> None: ... def pop(self, __index: int = ...) -> _T: ... # etc...
注意 _T
出现在 __init__
、append
和 extend
的参数中,以及 pop
的返回类型中。如果 _T
在 _T
中是协变或逆变的,那么没有办法使这样的类类型安全。
协变类型
考虑两种类型 A
和 B
,其中 B
与 A
一致,且它们都不是 Any
。一些作者使用 <:
和 :>
符号来表示这样的类型关系:
A :> B
A
是 B
的超类型或相同。
B <: A
B
是 A
的子类型或相同。
给定 A :> B
,泛型类型 C
在 C[A] :> C[B]
时是协变的。
注意 :>
符号的方向在 A
在 B
的左侧时是相同的。协变泛型类型遵循实际类型参数的子类型关系。
不可变容器可以是协变的。例如,typing.FrozenSet
类是如何 文档化 作为一个协变的,使用传统名称 T_co
的类型变量:
class FrozenSet(frozenset, AbstractSet[T_co]):
将 :>
符号应用于参数化类型,我们有:
float :> int frozenset[float] :> frozenset[int]
迭代器是协变泛型的另一个例子:它们不是只读集合,如 frozenset
,但它们只产生输出。任何期望一个产生浮点数的 abc.Iterator[float]
的代码可以安全地使用一个产生整数的 abc.Iterator[int]
。Callable
类型在返回类型上是协变的,原因类似。
逆变类型
给定 A :> B
,泛型类型 K
在 K[A] <: K[B]
时是逆变的。
逆变泛型类型颠倒了实际类型参数的子类型关系。
TrashCan
类是一个例子:
Refuse :> Biodegradable TrashCan[Refuse] <: TrashCan[Biodegradable]
逆变容器通常是一个只写数据结构,也称为“接收器”。标准库中没有这样的集合的例子,但有一些具有逆变类型参数的类型。
Callable[[ParamType, …], ReturnType]
在参数类型上是逆变的,但在 ReturnType
上是协变的,正如我们在 “Callable 类型的方差” 中看到的。此外,Generator
、Coroutine
和 AsyncGenerator
有一个逆变类型参数。Generator
类型在 “经典协程的泛型类型提示” 中有描述;Coroutine
和 AsyncGenerator
在 第二十一章 中有描述。
对于关于方差的讨论,主要观点是逆变的形式参数定义了用于调用或发送数据到对象的参数类型,而不同的协变形式参数定义了对象产生的输出类型——产生类型或返回类型,取决于对象。 “发送” 和 “产出” 的含义在 “经典协程” 中有解释。
我们可以从这些关于协变输出和逆变输入的观察中得出有用的指导方针。
协变的经验法则
最后,以下是一些关于推理方差时的经验法则:
- 如果一个形式类型参数定义了从对象中输出的数据类型,那么它可以是协变的。
- 如果形式类型参数定义了一个类型,用于在对象初始构建后进入对象的数据,它可以是逆变的。
- 如果形式类型参数定义了一个用于从对象中提取数据的类型,并且同一参数定义了一个用于将数据输入对象的类型,则它必须是不变的。
- 为了保险起见,使形式类型参数不变。
Callable[[ParamType, …], ReturnType]
展示了规则#1 和#2:ReturnType
是协变的,而每个ParamType
是逆变的。
默认情况下,TypeVar
创建的形式参数是不变的,这就是标准库中的可变集合是如何注释的。
“经典协程的通用类型提示”继续讨论关于方差的内容。
接下来,让我们看看如何定义通用的静态协议,将协变的思想应用到几个新的示例中。
实现通用的静态协议
Python 3.10 标准库提供了一些通用的静态协议。其中之一是SupportsAbs
,在typing 模块中实现如下:
@runtime_checkable class SupportsAbs(Protocol[T_co]): """An ABC with one abstract method __abs__ that is covariant in its return type.""" __slots__ = () @abstractmethod def __abs__(self) -> T_co: pass
T_co
根据命名约定声明:
T_co = TypeVar('T_co', covariant=True)
由于SupportsAbs
,Mypy 将此代码识别为有效,如您在示例 15-21 中所见。
示例 15-21。abs_demo.py:使用通用的SupportsAbs
协议
import math from typing import NamedTuple, SupportsAbs class Vector2d(NamedTuple): x: float y: float def __abs__(self) -> float: # ① return math.hypot(self.x, self.y) def is_unit(v: SupportsAbs[float]) -> bool: # ② """'True' if the magnitude of 'v' is close to 1.""" return math.isclose(abs(v), 1.0) # ③ assert issubclass(Vector2d, SupportsAbs) # ④ v0 = Vector2d(0, 1) # ⑤ sqrt2 = math.sqrt(2) v1 = Vector2d(sqrt2 / 2, sqrt2 / 2) v2 = Vector2d(1, 1) v3 = complex(.5, math.sqrt(3) / 2) v4 = 1 # ⑥ assert is_unit(v0) assert is_unit(v1) assert not is_unit(v2) assert is_unit(v3) assert is_unit(v4) print('OK')
①
定义__abs__
使Vector2d
与SupportsAbs
一致。
②
使用float
参数化SupportsAbs
确保…
③
…Mypy 接受abs(v)
作为math.isclose
的第一个参数。
④
在SupportsAbs
的定义中,感谢@runtime_checkable
,这是一个有效的运行时断言。
⑤
剩下的代码都通过了 Mypy 检查和运行时断言。
⑥
int
类型也与SupportsAbs
一致。根据typeshed,int.__abs__
返回一个int
,这与is_unit
类型提示中为v
参数声明的float
类型参数一致。
类似地,我们可以编写RandomPicker
协议的通用版本,该协议在示例 13-18 中介绍,该协议定义了一个返回Any
的单个方法pick
。
示例 15-22 展示了如何使通用的RandomPicker
在pick
的返回类型上具有协变性。
示例 15-22。generic_randompick.py:定义通用的RandomPicker
from typing import Protocol, runtime_checkable, TypeVar T_co = TypeVar('T_co', covariant=True) # ① @runtime_checkable class RandomPicker(Protocol[T_co]): # ② def pick(self) -> T_co: ... # ③
①
将T_co
声明为协变
。
②
这使RandomPicker
具有协变的形式类型参数。
③
使用T_co
作为返回类型。
通用的RandomPicker
协议可以是协变的,因为它的唯一形式参数用于返回类型。
有了这个,我们可以称之为一个章节。
章节总结
章节以一个简单的使用@overload
的例子开始,接着是一个我们详细研究的更复杂的例子:正确注释max
内置函数所需的重载签名。
接下来是typing.TypedDict
特殊构造。我选择在这里介绍它,而不是在第五章中看到typing.NamedTuple
,因为TypedDict
不是一个类构建器;它只是一种向需要具有特定一组字符串键和每个键特定类型的dict
添加类型提示的方式——当我们将dict
用作记录时,通常在处理 JSON 数据时会发生这种情况。该部分有点长,因为使用TypedDict
可能会给人一种虚假的安全感,我想展示在尝试将静态结构化记录转换为本质上是动态的映射时,运行时检查和错误处理是不可避免的。
接下来我们讨论了typing.cast
,这是一个旨在指导类型检查器工作的函数。仔细考虑何时使用cast
很重要,因为过度使用会妨碍类型检查器。
接下来是运行时访问类型提示。关键点是使用typing.get_type_hints
而不是直接读取__annotations__
属性。然而,该函数可能对某些注解不可靠,我们看到 Python 核心开发人员仍在努力找到一种方法,在减少对 CPU 和内存使用的影响的同时使类型提示在运行时可用。
最后几节是关于泛型的,首先是LottoBlower
泛型类——我们后来了解到它是一个不变的泛型类。该示例后面是四个基本术语的定义:泛型类型、形式类型参数、参数化类型和实际类型参数。
接下来介绍了主题的主要内容,使用自助餐厅饮料分配器和垃圾桶作为不变、协变和逆变通用类型的“现实生活”示例。接下来,我们对 Python 标准库中的示例进行了复习、形式化和进一步应用这些概念。
最后,我们看到了如何定义通用的静态协议,首先考虑typing.SupportsAbs
协议,然后将相同的思想应用于RandomPicker
示例,使其比第十三章中的原始协议更加严格。
注意
Python 的类型系统是一个庞大且快速发展的主题。本章不是全面的。我选择关注那些广泛适用、特别具有挑战性或在概念上重要且因此可能长期相关的主题。
进一步阅读
Python 的静态类型系统最初设计复杂,随着每年的发展变得更加复杂。表 15-1 列出了截至 2021 年 5 月我所知道的所有 PEP。要覆盖所有内容需要一整本书。
表 15-1。关于类型提示的 PEP,标题中带有链接。带有*号的 PEP 编号在typing
文档的开头段落中提到。Python 列中的问号表示正在讨论或尚未实施的 PEP;“n/a”出现在没有特定 Python 版本的信息性 PEP 中。
PEP | 标题 | Python | 年份 |
3107 | 函数注解 | 3.0 | 2006 |
483* | 类型提示理论 | n/a | 2014 |
484* | 类型提示 | 3.5 | 2014 |
482 | 类型提示文献综述 | n/a | 2015 |
526* | 变量注解的语法 | 3.6 | 2016 |
544* | 协议:结构子类型(静态鸭子类型) | 3.8 | 2017 |
557 | 数据类 | 3.7 | 2017 |
560 | 类型模块和泛型类型的核心支持 | 3.7 | 2017 |
561 | 分发和打包类型信息 | 3.7 | 2017 |
563 | 注解的延迟评估 | 3.7 | 2017 |
586* | 字面类型 | 3.8 | 2018 |
585 | 标准集合中的泛型类型提示 | 3.9 | 2019 |
589* | TypedDict:具有固定键集的字典的类型提示 | 3.8 | 2019 |
591* | 向 typing 添加 final 修饰符 | 3.8 | 2019 |
593 | 灵活的函数和变量注释 | ? | 2019 |
604 | 将联合类型写为 X | Y | 3.10 | 2019 |
612 | 参数规范变量 | 3.10 | 2019 |
613 | 显式类型别名 | 3.10 | 2020 |
645 | 允许将可选类型写为 x? | ? | 2020 |
646 | 可变泛型 | ? | 2020 |
647 | 用户定义的类型守卫 | 3.10 | 2021 |
649 | 使用描述符延迟评估注释 | ? | 2021 |
655 | 将个别 TypedDict 项目标记为必需或可能缺失 | ? | 2021 |
Python 的官方文档几乎无法跟上所有内容,因此Mypy 的文档是一个必不可少的参考。强大的 Python 作者:帕特里克·维亚福雷(O’Reilly)是我知道的第一本广泛涵盖 Python 静态类型系统的书籍,于 2021 年 8 月出版。你现在可能正在阅读第二本这样的书籍。
关于协变的微妙主题在 PEP 484 的章节中有专门讨论,同时也在 Mypy 的“泛型”页面以及其宝贵的“常见问题”页面中有涵盖。
阅读值得的PEP 362—函数签名对象,如果你打算使用补充typing.get_type_hints
函数的inspect
模块。
如果你对 Python 的历史感兴趣,你可能会喜欢知道,Guido van Rossum 在 2004 年 12 月 23 日发布了“向 Python 添加可选静态类型”。
“Python 3 中的类型在野外:两种类型系统的故事” 是由 Rensselaer Polytechnic Institute 和 IBM TJ Watson 研究中心的 Ingkarat Rak-amnouykit 等人撰写的研究论文。该论文调查了 GitHub 上开源项目中类型提示的使用情况,显示大多数项目并未使用它们,而且大多数具有类型提示的项目显然也没有使用类型检查器。我发现最有趣的是对 Mypy 和 Google 的 pytype 不同语义的讨论,他们得出结论称它们“本质上是两种不同的类型系统”。
两篇关于渐进式类型的重要论文是吉拉德·布拉查的“可插入式类型系统”,以及埃里克·迈杰和彼得·德雷顿撰写的“可能时使用静态类型,需要时使用动态类型:编程语言之间的冷战结束”¹⁷
通过阅读其他语言实现相同思想的一些书籍的相关部分,我学到了很多:
- 原子 Kotlin 作者:布鲁斯·埃克尔和斯维特兰娜·伊萨科娃(Mindview)
- Effective Java,第三版 作者:乔舒亚·布洛克(Addison-Wesley)
- 使用类型编程:TypeScript 示例 作者:弗拉德·里斯库蒂亚(Manning)
- 编程 TypeScript 作者:鲍里斯·切尔尼(O’Reilly)
- Dart 编程语言 作者:吉拉德·布拉查(Addison-Wesley)¹⁸
对于一些关于类型系统的批判观点,我推荐阅读维克多·尤代肯的文章“类型理论中的坏主意”和“类型有害 II”。
最后,我惊讶地发现了 Ken Arnold 的“泛型有害论”,他是 Java 的核心贡献者,也是官方Java 编程语言书籍(Addison-Wesley)前四版的合著者之一——与 Java 的首席设计师 James Gosling 合作。
遗憾的是,Arnold 的批评也适用于 Python 的静态类型系统。在阅读许多有关类型提示 PEP 的规则和特例时,我不断想起 Gosling 文章中的这段话:
这就提出了我总是为 C++引用的问题:我称之为“例外规则的 N^(th)次例外”。听起来是这样的:“你可以做 x,但在情况 y 下除外,除非 y 做 z,那么你可以如果…”
幸运的是,Python 比 Java 和 C++有一个关键优势:可选的类型系统。当类型提示变得太繁琐时,我们可以关闭类型检查器并省略类型提示。
¹ 来自 YouTube 视频“语言创作者对话:Guido van Rossum、James Gosling、Larry Wall 和 Anders Hejlsberg”,于 2019 年 4 月 2 日直播。引用开始于1:32:05,经过简化编辑。完整的文字记录可在https://github.com/fluentpython/language-creators找到。
² 我要感谢 Jelle Zijlstra——一个typeshed的维护者——教会了我很多东西,包括如何将我最初的九个重载减少到六个。
³ 截至 2020 年 5 月,pytype 允许这样做。但其常见问题解答中表示将来会禁止这样做。请参见 pytype常见问题解答中的“为什么 pytype 没有捕捉到我更改了已注释变量的类型?”问题。
⁴ 我更喜欢使用lxml包来生成和解析 XML:它易于上手,功能齐全且速度快。不幸的是,lxml 和 Python 自带的ElementTree不适用于我假想的微控制器的有限 RAM。
⁵ Mypy 文档在其“常见问题和解决方案”页面中讨论了这个问题,在“空集合的类型”一节中有详细说明。
⁶ Brett Cannon、Guido van Rossum 等人自 2016 年以来一直在讨论如何为json.loads()
添加类型提示,在Mypy 问题#182:定义 JSON 类型中。
⁷ 示例中使用enumerate
旨在混淆类型检查器。Mypy 可以正确分析直接生成字符串而不经过enumerate
索引的更简单的实现,因此不需要cast()
。
⁸ 我报告了typeshed的问题#5535,“asyncio.base_events.Server sockets 属性的错误类型提示”,Sebastian Rittau 很快就修复了。然而,我决定保留这个例子,因为它展示了cast
的一个常见用例,而我写的cast
是无害的。
⁹ 老实说,我最初在带有server.sockets[0]
的行末添加了一个# type: ignore
注释,因为经过一番调查,我在asyncio 文档和一个测试用例中找到了类似的行,所以我怀疑问题不在我的代码中。
¹⁰ 2020 年 5 月 19 日消息发送至 typing-sig 邮件列表。
¹¹ 语法# type: ignore[code]
允许您指定要消除的 Mypy 错误代码,但这些代码并不总是容易解释。请参阅 Mypy 文档中的“错误代码”。
¹² 我不会详细介绍 clip
的实现,但如果你感兴趣,可以阅读 clip_annot.py 中的整个模块。
¹³ 2021 年 4 月 16 日发布的信息 “PEP 563 in light of PEP 649”。
¹⁴ 这些术语来自 Joshua Bloch 的经典著作 Effective Java,第三版(Addison-Wesley)。定义和示例是我自己的。
¹⁵ 我第一次看到 Erik Meijer 在 Gilad Bracha 的 The Dart Programming Language 一书(Addison-Wesley)的 前言 中使用自助餐厅类比来解释方差。
¹⁶ 比禁书好多了!
¹⁷ 作为脚注的读者,你可能记得我将 Erik Meijer 归功于用自助餐厅类比来解释方差。
¹⁸ 那本书是为 Dart 1 写的。Dart 2 有重大变化,包括类型系统。尽管如此,Bracha 是编程语言设计领域的重要研究者,我发现这本书对 Dart 的设计视角很有价值。
¹⁹ 参见 PEP 484 中 “Covariance and Contravariance” 部分的最后一段。
第十六章:运算符重载
有一些事情让我感到矛盾,比如运算符重载。我在 C++ 中看到太多人滥用它,所以我把运算符重载略去了,这是一个相当个人的选择。
Java 的创始人詹姆斯·高斯林¹
在 Python 中,你可以使用以下公式计算复利:
interest = principal * ((1 + rate) ** periods - 1)
出现在操作数之间的运算符,如 1 + rate
,是中缀运算符。在 Python 中,中缀运算符可以处理任意类型。因此,如果你处理真实货币,你可以确保 principal
、rate
和 periods
是精确的数字 —— Python decimal.Decimal
类的实例 —— 并且该公式将按照写入的方式工作,产生精确的结果。
但是在 Java 中,如果你从 float
切换到 BigDecimal
以获得精确的结果,你就不能再使用中缀运算符了,因为它们只适用于原始类型。这是在 Java 中使用 BigDecimal
数字编写的相同公式:
BigDecimal interest = principal.multiply(BigDecimal.ONE.add(rate) .pow(periods).subtract(BigDecimal.ONE));
显然,中缀运算符使公式更易读。运算符重载是支持用户定义或扩展类型的中缀运算符表示法的必要条件,例如 NumPy 数组。在一个高级、易于使用的语言中具有运算符重载可能是 Python 在数据科学领域取得巨大成功的关键原因,包括金融和科学应用。
在“模拟数值类型”(第一章)中,我们看到了一个简单的 Vector
类中运算符的实现。示例 1-2 中的 __add__
和 __mul__
方法是为了展示特殊方法如何支持运算符重载,但是它们的实现中存在一些微妙的问题被忽略了。此外,在示例 11-2 中,我们注意到 Vector2d.__eq__
方法认为这是 True
:Vector(3, 4) == [3, 4]
——这可能有或没有意义。我们将在本章中解决这些问题,以及:
- 中缀运算符方法应如何表示无法处理操作数
- 使用鸭子类型或鹅类型处理各种类型的操作数
- 丰富比较运算符的特殊行为(例如,
==
,>
,<=
等) - 增强赋值运算符(如
+=
)的默认处理方式,以及如何对其进行重载
本章的新内容
鹅类型是 Python 的一个关键部分,但 numbers
ABCs 在静态类型中不受支持,因此我改变了示例 16-11 以使用鸭子类型而不是针对 numbers.Real
的显式 isinstance
检查。²
我在第一版的 Fluent Python 中介绍了 @
矩阵乘法运算符,当 3.5 版本还处于 alpha 阶段时,它被视为即将到来的变化。因此,该运算符不再是一个旁注,而是在“使用 @ 作为中缀运算符”的章节流中整合了进去。我利用鹅类型使 __matmul__
的实现比第一版更安全,而不会影响灵活性。
“进一步阅读” 现在有几个新的参考资料 —— 包括 Guido van Rossum 的一篇博客文章。我还添加了两个展示运算符重载在数学领域之外有效使用的库:pathlib
和 Scapy
。
运算符重载 101
运算符重载允许用户定义的对象与中缀运算符(如 +
和 |
)或一元运算符(如 -
和 ~
)进行交互。更一般地说,函数调用(()
)、属性访问(.
)和项目访问/切片([]
)在 Python 中也是运算符,但本章涵盖一元和中缀运算符。
运算符重载在某些圈子里名声不佳。这是一种语言特性,可能会被滥用,导致程序员困惑、错误和意外的性能瓶颈。但如果使用得当,它会导致愉快的 API 和可读的代码。Python 在灵活性、可用性和安全性之间取得了良好的平衡,通过施加一些限制:
- 我们不能改变内置类型的运算符的含义。
- 我们不能创建新的运算符,只能重载现有的运算符。
- 有一些运算符无法重载:
is
,and
,or
,not
(但位运算符&
,|
,~
可以)。
在第十二章中,我们已经在Vector
中有一个中缀运算符:==
,由__eq__
方法支持。在本章中,我们将改进__eq__
的实现,以更好地处理除Vector
之外的类型的操作数。然而,富比较运算符(==
,!=
,>
,<
,>=
,<=
)是运算符重载中的特殊情况,因此我们将从重载Vector
中的四个算术运算符开始:一元-
和+
,然后是中缀+
和*
。
让我们从最简单的话题开始:一元运算符。
一元运算符
Python 语言参考,“6.5. 一元算术和位运算”列出了三个一元运算符,这里显示它们及其相关的特殊方法:
-
,由__neg__
实现
算术一元取反。如果x
是-2
,那么-x == 2
。
+
,由__pos__
实现
算术一元加号。通常x == +x
,但也有一些情况不成立。如果你感兴趣,可以查看“当 x 和 +x 不相等时”。
~
,由__invert__
实现
位取反,或整数的位反,定义为~x == -(x+1)
。如果x
是2
,那么~x == -3
。³
Python 语言参考的“数据模型”章节还将abs()
内置函数列为一元运算符。相关的特殊方法是__abs__
,正如我们之前看到的。
支持一元运算符很容易。只需实现适当的特殊方法,该方法只接受一个参数:self
。在类中使用适当的逻辑,但遵循运算符的一般规则:始终返回一个新对象。换句话说,不要修改接收者(self
),而是创建并返回一个适当类型的新实例。
对于-
和+
,结果可能是与self
相同类的实例。对于一元+
,如果接收者是不可变的,则应返回self
;否则,返回self
的副本。对于abs()
,结果应该是一个标量数字。
至于~
,如果不处理整数中的位,很难说会得到什么合理的结果。在pandas数据分析包中,波浪线对布尔过滤条件取反;请参阅pandas文档中的“布尔索引”以获取示例。
正如之前承诺的,我们将在第十二章的Vector
类上实现几个新的运算符。示例 16-1 展示了我们已经在示例 12-16 中拥有的__abs__
方法,以及新添加的__neg__
和__pos__
一元运算符方法。
示例 16-1. vector_v6.py:一元运算符 - 和 + 添加到示例 12-16
def __abs__(self): return math.hypot(*self) def __neg__(self): return Vector(-x for x in self) # ① def __pos__(self): return Vector(self) # ②
①
要计算-v
,构建一个新的Vector
,其中包含self
的每个分量的取反。
②
要计算+v
,构建一个新的Vector
,其中包含self
的每个分量。
请记住,Vector
实例是可迭代的,Vector.__init__
接受一个可迭代的参数,因此__neg__
和__pos__
的实现简洁明了。
我们不会实现__invert__
,因此如果用户在Vector
实例上尝试~v
,Python 将引发TypeError
并显示清晰的消息:“一元~的错误操作数类型:'Vector'
。”
以下侧边栏涵盖了一个关于一元+
的好奇心,也许有一天可以帮你赢得一次赌注。
重载 + 实现向量加法
Vector
类是一个序列类型,在官方 Python 文档的“数据模型”章节中的“3.3.6. 模拟容器类型”部分指出,序列应该支持+
运算符进行连接和*
进行重复。然而,在这里我们将实现+
和*
作为数学向量运算,这有点困难,但对于Vector
类型更有意义。
提示
如果用户想要连接或重复Vector
实例,他们可以将其转换为元组或列表,应用运算符,然后再转换回来——这要归功于Vector
是可迭代的,并且可以从可迭代对象构建:
>>> v_concatenated = Vector(list(v1) + list(v2)) >>> v_repeated = Vector(tuple(v1) * 5)
将两个欧几里德向量相加会得到一个新的向量,其中的分量是操作数的分量的成对相加。举例说明:
>>> v1 = Vector([3, 4, 5]) >>> v2 = Vector([6, 7, 8]) >>> v1 + v2 Vector([9.0, 11.0, 13.0]) >>> v1 + v2 == Vector([3 + 6, 4 + 7, 5 + 8]) True
如果我们尝试将长度不同的两个Vector
实例相加会发生什么?我们可以引发一个错误,但考虑到实际应用(如信息检索),最好是用零填充最短的Vector
。这是我们想要的结果:
>>> v1 = Vector([3, 4, 5, 6]) >>> v3 = Vector([1, 2]) >>> v1 + v3 Vector([4.0, 6.0, 5.0, 6.0])
鉴于这些基本要求,我们可以像示例 16-4 中那样实现__add__
。
示例 16-4. Vector.__add__
方法,第一种情况
# inside the Vector class def __add__(self, other): pairs = itertools.zip_longest(self, other, fillvalue=0.0) # ① return Vector(a + b for a, b in pairs) # ②
①
pairs
是一个生成器,产生元组(a, b)
,其中a
来自self
,b
来自other
。如果self
和other
的长度不同,fillvalue
会为最短的可迭代对象提供缺失值。
②
从生成器表达式构建一个新的Vector
,为pairs
中的每个(a, b)
执行一次加法。
注意__add__
如何返回一个新的Vector
实例,并且不改变self
或other
。
警告
实现一元或中缀运算符的特殊方法永远不应更改操作数的值。带有这些运算符的表达式预期通过创建新对象来产生结果。只有增强赋值运算符可以更改第一个操作数(self
),如“增强赋值运算符”中所讨论的。
示例 16-4 允许将Vector
添加到Vector2d
,以及将Vector
添加到元组或任何产生数字的可迭代对象,正如示例 16-5 所证明的那样。
示例 16-5. Vector.__add__
第一种情况也支持非Vector
对象
>>> v1 = Vector([3, 4, 5]) >>> v1 + (10, 20, 30) Vector([13.0, 24.0, 35.0]) >>> from vector2d_v3 import Vector2d >>> v2d = Vector2d(1, 2) >>> v1 + v2d Vector([4.0, 6.0, 5.0])
示例 16-5 中+
的两种用法都有效,因为__add__
使用了zip_longest(…)
,它可以消耗任何可迭代对象,并且用于构建新Vector
的生成器表达式仅执行zip_longest(…)
产生的对中的a + b
,因此产生任意数量项的可迭代对象都可以。
然而,如果我们交换操作数(示例 16-6),混合类型的加法会失败。
示例 16-6. Vector.__add__
第一种情况在非Vector
左操作数上失败
>>> v1 = Vector([3, 4, 5]) >>> (10, 20, 30) + v1 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: can only concatenate tuple (not "Vector") to tuple >>> from vector2d_v3 import Vector2d >>> v2d = Vector2d(1, 2) >>> v2d + v1 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unsupported operand type(s) for +: 'Vector2d' and 'Vector'
为了支持涉及不同类型对象的操作,Python 为中缀运算符特殊方法实现了一种特殊的调度机制。给定表达式a + b
,解释器将执行以下步骤(也参见图 16-1):
- 如果
a
有__add__
,则调用a.__add__(b)
并返回结果,除非它是NotImplemented
。 - 如果
a
没有__add__
,或者调用它返回NotImplemented
,则检查b
是否有__radd__
,然后调用b.__radd__(a)
并返回结果,除非它是NotImplemented
。 - 如果
b
没有__radd__
,或者调用它返回NotImplemented
,则引发TypeError
,并显示不支持的操作数类型消息。
提示
__radd__
方法被称为__add__
的“反射”或“反转”版本。我更喜欢称它们为“反转”特殊方法。⁴
图 16-1. 使用__add__
和__radd__
计算a + b
的流程图。
因此,为了使示例 16-6 中的混合类型加法起作用,我们需要实现Vector.__radd__
方法,如果左操作数不实现__add__
,或者实现了但返回NotImplemented
以表示不知道如何处理右操作数,则 Python 将调用它作为后备。
警告
不要混淆NotImplemented
和NotImplementedError
。第一个NotImplemented
是一个特殊的单例值,中缀运算符特殊方法应该返回
以告诉解释器它无法处理给定的操作数。相反,NotImplementedError
是一个异常,抽象类中的存根方法可能会引发
以警告子类必须实现它们。
__radd__
的最简单的工作实现在示例 16-7 中显示。
示例 16-7. Vector
方法__add__
和__radd__
# inside the Vector class def __add__(self, other): # ① pairs = itertools.zip_longest(self, other, fillvalue=0.0) return Vector(a + b for a, b in pairs) def __radd__(self, other): # ② return self + other
①
与示例 16-4 中的__add__
没有变化;这里列出是因为__radd__
使用它。
②
__radd__
只是委托给__add__
。
__radd__
通常很简单:只需调用适当的运算符,因此在这种情况下委托给__add__
。这适用于任何可交换的运算符;当处理数字或我们的向量时,+
是可交换的,但在 Python 中连接序列时不是可交换的。
如果__radd__
简单地调用__add__
,那么这是实现相同效果的另一种方法:
def __add__(self, other): pairs = itertools.zip_longest(self, other, fillvalue=0.0) return Vector(a + b for a, b in pairs) __radd__ = __add__
示例 16-7 中的方法适用于Vector
对象,或具有数字项的任何可迭代对象,例如Vector2d
,一组整数的tuple
,或一组浮点数的array
。但如果提供了一个不可迭代的对象,__add__
将引发一个带有不太有用消息的异常,就像示例 16-8 中一样。
示例 16-8. Vector.__add__
方法需要一个可迭代的操作数
>>> v1 + 1 Traceback (most recent call last): File "<stdin>", line 1, in <module> File "vector_v6.py", line 328, in __add__ pairs = itertools.zip_longest(self, other, fillvalue=0.0) TypeError: zip_longest argument #2 must support iteration
更糟糕的是,如果一个操作数是可迭代的,但其项无法添加到Vector
中的float
项中,则会得到一个误导性的消息。请参见示例 16-9。
示例 16-9. Vector.__add__
方法需要具有数字项的可迭代对象
>>> v1 + 'ABC' Traceback (most recent call last): File "<stdin>", line 1, in <module> File "vector_v6.py", line 329, in __add__ return Vector(a + b for a, b in pairs) File "vector_v6.py", line 243, in __init__ self._components = array(self.typecode, components) File "vector_v6.py", line 329, in <genexpr> return Vector(a + b for a, b in pairs) TypeError: unsupported operand type(s) for +: 'float' and 'str'
我尝试添加Vector
和一个str
,但消息抱怨float
和str
。
示例 16-8 和 16-9 中的问题实际上比晦涩的错误消息更深:如果一个运算符特殊方法由于类型不兼容而无法返回有效结果,它应该返回NotImplemented
而不是引发TypeError
。通过返回NotImplemented
,您为另一个操作数类型的实现者留下了机会,在 Python 尝试调用反向方法时执行操作。
符合鸭子类型的精神,我们将避免测试other
操作数的类型,或其元素的类型。我们将捕获异常并返回NotImplemented
。如果解释器尚未颠倒操作数,则将尝试这样做。如果反向方法调用返回NotImplemented
,那么 Python 将引发TypeError
,并显示标准错误消息,如“不支持的操作数类型:Vector和str”。
Vector
加法的特殊方法的最终实现在示例 16-10 中。
流畅的 Python 第二版(GPT 重译)(八)(4)https://developer.aliyun.com/article/1484692