流畅的 Python 第二版(GPT 重译)(七)(2)

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

流畅的 Python 第二版(GPT 重译)(七)(1)https://developer.aliyun.com/article/1484634

图 13-6。Exception类层次结构的一部分。¹¹

LookupError是我们在Tombola.inspect中处理的异常。

IndexError是我们尝试从序列中获取超出最后位置的索引时引发的LookupError子类。

当我们使用不存在的键从映射中获取项时,会引发KeyError

现在我们有了自己的Tombola ABC。为了见证 ABC 执行的接口检查,让我们尝试用一个有缺陷的实现来愚弄Tombola,参见示例 13-8。

示例 13-8。一个虚假的Tombola不会被发现
>>> from tombola import Tombola
>>> class Fake(Tombola):  # ①
...     def pick(self):
...         return 13
...
>>> Fake  # ②
<class '__main__.Fake'> >>> f = Fake()  # ③
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class Fake with abstract method load

Fake声明为Tombola的子类。

类已创建,目前没有错误。

当我们尝试实例化Fake时,会引发TypeError。消息非常清楚:Fake被视为抽象,因为它未能实现Tombola ABC 中声明的抽象方法之一load

所以我们定义了我们的第一个 ABC,并让它验证一个类的工作。我们很快将子类化Tombola ABC,但首先我们必须了解一些 ABC 编码规则。

ABC 语法细节

声明 ABC 的标准方式是继承abc.ABC或任何其他 ABC。

除了ABC基类和@abstractmethod装饰器外,abc模块还定义了@abstractclassmethod@abstractstaticmethod@abstractproperty装饰器。然而,在 Python 3.3 中,这三个装饰器已被弃用,因为现在可以在@abstractmethod之上堆叠装饰器,使其他装饰器变得多余。例如,声明抽象类方法的首选方式是:

class MyABC(abc.ABC):
    @classmethod
    @abc.abstractmethod
    def an_abstract_classmethod(cls, ...):
        pass
警告

堆叠函数装饰器的顺序很重要,在@abstractmethod的情况下,文档是明确的:

abstractmethod()与其他方法描述符结合使用时,应将其应用为最内层的装饰器…¹²

换句话说,在@abstractmethoddef语句之间不得出现其他装饰器。

现在我们已经解决了这些 ABC 语法问题,让我们通过实现两个具体的子类来使用Tombola

ABC 的子类化

鉴于Tombola ABC,我们现在将开发两个满足其接口的具体子类。这些类在图 13-5 中有所描述,以及下一节将讨论的虚拟子类。

示例 13-9 中的BingoCage类是示例 7-8 的变体,使用了更好的随机化程序。这个BingoCage实现了所需的抽象方法loadpick

示例 13-9。bingo.py:BingoCageTombola的具体子类
import random
from tombola import Tombola
class BingoCage(Tombola):  # ①
    def __init__(self, items):
        self._randomizer = random.SystemRandom()  # ②
        self._items = []
        self.load(items)  # ③
    def load(self, items):
        self._items.extend(items)
        self._randomizer.shuffle(self._items)  # ④
    def pick(self):  # ⑤
        try:
            return self._items.pop()
        except IndexError:
            raise LookupError('pick from empty BingoCage')
    def __call__(self):  # ⑥
        self.pick()

这个BingoCage类明确扩展了Tombola

假设我们将用于在线游戏。random.SystemRandomos.urandom(…)函数之上实现了random API,该函数提供了“适用于加密用途”的随机字节,根据os模块文档

将初始加载委托给.load(…)方法。

我们使用我们的SystemRandom实例的.shuffle()方法,而不是普通的random.shuffle()函数。

pick的实现如示例 7-8 中所示。

__call__也来自示例 7-8。虽然不需要满足Tombola接口,但添加额外的方法也没有坏处。

BingoCage继承了Tombola的昂贵loaded和愚蠢的inspect方法。都可以用快得多的一行代码重写,就像示例 13-10 中那样。关键是:我们可以懒惰并只继承来自 ABC 的次优具体方法。从Tombola继承的方法对于BingoCage来说并不像它们本应该的那样快,但对于任何正确实现pickloadTombola子类,它们确实提供了正确的结果。

示例 13-10 展示了Tombola接口的一个非常不同但同样有效的实现。LottoBlower不是洗“球”并弹出最后一个,而是从随机位置弹出。

示例 13-10。lotto.py:LottoBlower是一个具体子类,覆盖了Tombolainspectloaded方法。
import random
from tombola import Tombola
class LottoBlower(Tombola):
    def __init__(self, iterable):
        self._balls = list(iterable)  # ①
    def load(self, iterable):
        self._balls.extend(iterable)
    def pick(self):
        try:
            position = random.randrange(len(self._balls))  # ②
        except ValueError:
            raise LookupError('pick from empty LottoBlower')
        return self._balls.pop(position)  # ③
    def loaded(self):  # ④
        return bool(self._balls)
    def inspect(self):  # ⑤
        return tuple(self._balls)

初始化程序接受任何可迭代对象:该参数用于构建一个列表。

random.randrange(…)函数在范围为空时会引发ValueError,因此我们捕获并抛出LookupError,以便与Tombola兼容。

否则,随机选择的项目将从self._balls中弹出。

重写loaded以避免调用inspect(就像示例 13-7 中的Tombola.loaded一样)。通过直接使用self._balls来工作,我们可以使其更快—不需要构建一个全新的tuple

用一行代码重写inspect

示例 13-10 展示了一个值得一提的习惯用法:在__init__中,self._balls存储list(iterable)而不仅仅是iterable的引用(即,我们并没有简单地赋值self._balls = iterable,从而给参数起了个别名)。正如在“防御性编程和‘快速失败’”中提到的,这使得我们的LottoBlower灵活,因为iterable参数可以是任何可迭代类型。同时,我们确保将其项存储在list中,这样我们就可以pop项。即使我们总是得到列表作为iterable参数,list(iterable)也会产生参数的副本,这是一个很好的做法,考虑到我们将从中删除项目,而客户端可能不希望提供的列表被更改。¹³

现在我们来到鹅类型的关键动态特性:使用register方法声明虚拟子类。

一个 ABC 的虚拟子类

鹅类型的一个重要特征——也是为什么它值得一个水禽名字的原因之一——是能够将一个类注册为 ABC 的虚拟子类,即使它没有继承自它。这样做时,我们承诺该类忠实地实现了 ABC 中定义的接口——Python 会相信我们而不进行检查。如果我们撒谎,我们将被通常的运行时异常捕获。

这是通过在 ABC 上调用register类方法来完成的。注册的类然后成为 ABC 的虚拟子类,并且将被issubclass识别为这样,但它不会继承 ABC 的任何方法或属性。

警告

虚拟子类不会从其注册的 ABC 继承,并且在任何时候都不会检查其是否符合 ABC 接口,即使在实例化时也是如此。此外,静态类型检查器目前无法处理虚拟子类。详情请参阅Mypy issue 2922—ABCMeta.register support

register方法通常作为一个普通函数调用(参见“实践中的 register 用法”),但也可以用作装饰器。在示例 13-11 中,我们使用装饰器语法并实现TomboListTombola的虚拟子类,如图 13-7 所示。

图 13-7. TomboList的 UML 类图,list的真实子类和Tombola的虚拟子类。
示例 13-11. tombolist.py:类TomboListTombola的虚拟子类
from random import randrange
from tombola import Tombola
@Tombola.register  # ①
class TomboList(list):  # ②
    def pick(self):
        if self:  # ③
            position = randrange(len(self))
            return self.pop(position)  # ④
        else:
            raise LookupError('pop from empty TomboList')
    load = list.extend  # ⑤
    def loaded(self):
        return bool(self)  # ⑥
    def inspect(self):
        return tuple(self)
# Tombola.register(TomboList) # ⑦
• 1

Tombolist被注册为Tombola的虚拟子类。

Tombolist扩展了list

Tombolistlist继承其布尔行为,如果列表不为空则返回True

我们的pick调用self.pop,从list继承,传递一个随机的项目索引。

Tombolist.loadlist.extend相同。

loaded委托给bool。¹⁴

总是可以以这种方式调用register,当你需要注册一个你不维护但符合接口的类时,这样做是很有用的。

请注意,由于注册,函数issubclassisinstance的行为就好像TomboListTombola的子类一样:

>>> from tombola import Tombola
>>> from tombolist import TomboList
>>> issubclass(TomboList, Tombola)
True
>>> t = TomboList(range(100))
>>> isinstance(t, Tombola)
True

然而,继承受到一个特殊的类属性__mro__的指导——方法解析顺序。它基本上按照 Python 用于搜索方法的顺序列出了类及其超类。¹⁵ 如果你检查TomboList__mro__,你会看到它只列出了“真正”的超类——listobject

>>> TomboList.__mro__
(<class 'tombolist.TomboList'>, <class 'list'>, <class 'object'>)

Tombola不在Tombolist.__mro__中,所以Tombolist不会从Tombola继承任何方法。

这结束了我们的TombolaABC 案例研究。在下一节中,我们将讨论registerABC 函数在实际中的使用方式。

实践中的 register 用法

在示例 13-11 中,我们使用Tombola.register作为一个类装饰器。在 Python 3.3 之前,register 不能像那样使用——它必须在类定义之后作为一个普通函数调用,就像示例 13-11 末尾的注释建议的那样。然而,即使现在,它更广泛地被用作一个函数来注册在其他地方定义的类。例如,在collections.abc模块的源代码中,内置类型tuplestrrangememoryview被注册为Sequence的虚拟子类,就像这样:

Sequence.register(tuple)
Sequence.register(str)
Sequence.register(range)
Sequence.register(memoryview)

其他几种内置类型在*_collections_abc.py*中被注册为 ABC。这些注册只会在导入该模块时发生,这是可以接受的,因为你无论如何都需要导入它来获取 ABC。例如,你需要从collections.abc导入MutableMapping来执行类似isinstance(my_dict, MutableMapping)的检查。

对 ABC 进行子类化或向 ABC 注册都是显式使我们的类通过issubclass检查的方法,以及依赖于issubclassisinstance检查。但有些 ABC 也支持结构化类型。下一节将解释。

带有 ABCs 的结构化类型

ABC 主要与名义类型一起使用。当类Sub明确从AnABC继承,或者与AnABC注册时,AnABC的名称与Sub类关联起来—这就是在运行时,issubclass(AnABC, Sub)返回True的原因。

相比之下,结构类型是通过查看对象的公共接口结构来确定其类型的:如果一个对象实现了类型定义中定义的方法,则它与该类型一致。动态和静态鸭子类型是结构类型的两种方法。

事实证明,一些 ABC 也支持结构类型。在他的文章“水禽和 ABC”中,Alex 表明一个类即使没有注册也可以被识别为 ABC 的子类。以下是他的例子,增加了使用issubclass的测试:

>>> class Struggle:
...     def __len__(self): return 23
...
>>> from collections import abc
>>> isinstance(Struggle(), abc.Sized)
True
>>> issubclass(Struggle, abc.Sized)
True

Struggleissubclass函数认为是abc.Sized的子类(因此,也被isinstance认为是)因为abc.Sized实现了一个名为__subclasshook__的特殊类方法。

Sized__subclasshook__检查类参数是否有名为__len__的属性。如果有,那么它被视为Sized的虚拟子类。参见示例 13-12。

示例 13-12。来自Lib/_collections_abc.py源代码中Sized的定义
class Sized(metaclass=ABCMeta):
    __slots__ = ()
    @abstractmethod
    def __len__(self):
        return 0
    @classmethod
    def __subclasshook__(cls, C):
        if cls is Sized:
            if any("__len__" in B.__dict__ for B in C.__mro__):  # ①
                return True  # ②
        return NotImplemented  # ③

如果在C.__mro__中列出的任何类(即C及其超类)的__dict__中有名为__len__的属性…

…返回True,表示CSized的虚拟子类。

否则返回NotImplemented以让子类检查继续进行。

注意

如果你对子类检查的细节感兴趣,请查看 Python 3.6 中ABCMeta.__subclasscheck__方法的源代码:Lib/abc.py。注意:它有很多的 if 语句和两次递归调用。在 Python 3.7 中,Ivan Levkivskyi 和 Inada Naoki 为了更好的性能,用 C 重写了abc模块的大部分逻辑。参见Python 问题 #31333。当前的ABCMeta.__subclasscheck__实现只是调用了_abc_subclasscheck。相关的 C 源代码在cpython/Modules/_abc.c#L605中。

这就是__subclasshook__如何使 ABC 支持结构类型。你可以用 ABC 规范化一个接口,可以对该 ABC 进行isinstance检查,而仍然可以让一个完全不相关的类通过issubclass检查,因为它实现了某个方法(或者因为它做了足够的事情来说服__subclasshook__为它背书)。

在我们自己的 ABC 中实现__subclasshook__是个好主意吗?可能不是。我在 Python 源代码中看到的所有__subclasshook__的实现都在像Sized这样声明了一个特殊方法的 ABC 中,它们只是检查那个特殊方法的名称。鉴于它们的“特殊”地位,你可以非常确定任何名为__len__的方法都会按照你的期望工作。但即使在特殊方法和基本 ABC 的领域,做出这样的假设也是有风险的。例如,映射实现了__len____getitem____iter__,但它们被正确地不认为是Sequence的子类型,因为你不能使用整数偏移或切片检索项目。这就是为什么abc.Sequence类不实现__subclasshook__

对于你和我可能编写的 ABCs,__subclasshook__可能会更不可靠。我不准备相信任何实现或继承loadpickinspectloadedSpam类都能保证像Tombola一样行为。最好让程序员通过将SpamTombola继承或使用Tombola.register(Spam)来确认。当然,你的__subclasshook__也可以检查方法签名和其他特性,但我认为这并不值得。

静态协议

注意

静态协议是在“静态协议”(第八章)中引入的。我考虑延迟对协议的所有覆盖,直到本章,但决定最初在函数中的类型提示的介绍中包括协议,因为鸭子类型是 Python 的一个重要部分,而没有协议的静态类型检查无法很好地处理 Pythonic API。

我们将通过两个简单示例和对数字 ABCs 和协议的讨论来结束本章。让我们首先展示静态协议如何使得我们可以对我们在“类型由支持的操作定义”中首次看到的double()函数进行注释和类型检查。

有类型的 double 函数

当向更习惯于静态类型语言的程序员介绍 Python 时,我最喜欢的一个例子就是这个简单的double函数:

>>> def double(x):
...     return x * 2
...
>>> double(1.5)
3.0
>>> double('A')
'AA'
>>> double([10, 20, 30])
[10, 20, 30, 10, 20, 30]
>>> from fractions import Fraction
>>> double(Fraction(2, 5))
Fraction(4, 5)

在引入静态协议之前,没有实际的方法可以为double添加类型提示,而不限制其可能的用途。¹⁷

由于鸭子类型的存在,double甚至可以与未来的类型一起使用,比如我们将在“为标量乘法重载 *”(第十六章)中看到的增强Vector类:

>>> from vector_v7 import Vector
>>> double(Vector([11.0, 12.0, 13.0]))
Vector([22.0, 24.0, 26.0])

Python 中类型提示的初始实现是一种名义类型系统:注释中的类型名称必须与实际参数的类型名称或其超类的名称匹配。由于不可能命名所有支持所需操作的协议的类型,因此在 Python 3.8 之前无法通过类型提示描述鸭子类型。

现在,通过typing.Protocol,我们可以告诉 Mypy,double接受一个支持x * 2的参数x。示例 13-13 展示了如何实现。

示例 13-13. double_protocol.py: 使用Protocol定义double的定义
from typing import TypeVar, Protocol
T = TypeVar('T')  # ①
class Repeatable(Protocol):
    def __mul__(self: T, repeat_count: int) -> T: ...  # ②
RT = TypeVar('RT', bound=Repeatable)  # ③
def double(x: RT) -> RT:  # ④
    return x * 2

我们将在__mul__签名中使用这个T

__mul__Repeatable协议的本质。self参数通常不会被注释,其类型被假定为类。在这里,我们使用T来确保结果类型与self的类型相同。此外,请注意,此协议中的repeat_count限制为int

RT类型变量受Repeatable协议的约束:类型检查器将要求实际类型实现Repeatable

现在类型检查器能够验证x参数是一个可以乘以整数的对象,并且返回值与x的类型相同。

本示例说明了为什么PEP 544的标题是“协议:结构子类型(静态鸭子类型)”。给定给double的实际参数x的名义类型是无关紧要的,只要它呱呱叫,也就是说,只要它实现了__mul__

可运行时检查的静态协议

在类型映射中(图 13-1),typing.Protocol出现在静态检查区域—图表的下半部分。然而,当定义typing.Protocol子类时,您可以使用@runtime_checkable装饰器使该协议支持运行时的isinstance/issubclass检查。这是因为typing.Protocol是一个 ABC,因此支持我们在“使用 ABC 进行结构化类型检查”中看到的__subclasshook__

截至 Python 3.9,typing模块包含了七个可供直接使用的运行时可检查的协议。以下是其中两个,直接引用自typing文档

class typing.SupportsComplex

一个具有一个抽象方法__complex__的 ABC。

class typing.SupportsFloat

一个具有一个抽象方法__float__的 ABC。

这些协议旨在检查数值类型的“可转换性”:如果一个对象o实现了__complex__,那么您应该能够通过调用complex(o)来获得一个complex——因为__complex__特殊方法存在是为了支持complex()内置函数。

示例 13-14 展示了typing.SupportsComplex协议的源代码

示例 13-14. typing.SupportsComplex协议源代码
@runtime_checkable
class SupportsComplex(Protocol):
    """An ABC with one abstract method __complex__."""
    __slots__ = ()
    @abstractmethod
    def __complex__(self) -> complex:
        pass

关键在于__complex__抽象方法。¹⁸ 在静态类型检查期间,如果一个对象实现了只接受self并返回complex__complex__方法,则该对象将被视为与SupportsComplex协议一致

由于@runtime_checkable类装饰器应用于SupportsComplex,因此该协议也可以与isinstance检查一起在示例 13-15 中使用。

示例 13-15. 在运行时使用SupportsComplex
>>> from typing import SupportsComplex
>>> import numpy as np
>>> c64 = np.complex64(3+4j)  # ①
>>> isinstance(c64, complex)   # ②
False >>> isinstance(c64, SupportsComplex)  # ③
True >>> c = complex(c64)  # ④
>>> c
(3+4j) >>> isinstance(c, SupportsComplex) # ⑤
False >>> complex(c)
(3+4j)

complex64是 NumPy 提供的五种复数类型之一。

NumPy 的任何复数类型都不是内置的complex的子类。

但 NumPy 的复数类型实现了__complex__,因此它们符合SupportsComplex协议。

因此,您可以从中创建内置的complex对象。

遗憾的是,complex内置类型不实现__complex__,尽管如果ccomplex,那么complex(c)可以正常工作。

由于上述最后一点,如果您想测试对象c是否为complexSupportsComplex,您可以将类型元组作为isinstance的第二个参数提供,就像这样:

isinstance(c, (complex, SupportsComplex))

另一种方法是使用numbers模块中定义的Complex ABC。内置的complex类型和 NumPy 的complex64complex128类型都注册为numbers.Complex的虚拟子类,因此这样可以工作:

>>> import numbers
>>> isinstance(c, numbers.Complex)
True
>>> isinstance(c64, numbers.Complex)
True

在第一版的流畅的 Python中,我推荐使用numbers ABCs,但现在这不再是一个好建议,因为这些 ABCs 不被静态类型检查器识别,正如我们将在“数字 ABC 和数值协议”中看到的那样。

在本节中,我想演示一个运行时可检查的协议如何与isinstance一起工作,但事实证明这个示例并不是isinstance的一个特别好的用例,因为侧边栏“鸭子类型是你的朋友”解释了这一点。

提示

如果您正在使用外部类型检查器,那么显式的isinstance检查有一个优点:当您编写一个条件为isinstance(o, MyType)if语句时,那么 Mypy 可以推断在if块内,o对象的类型与MyType一致

现在我们已经看到如何在运行时使用静态协议与预先存在的类型如complexnumpy.complex64,我们需要讨论运行时可检查协议的限制。

运行时协议检查的限制

我们已经看到类型提示通常在运行时被忽略,这也影响了对静态协议进行isinstanceissubclass检查。

例如,任何具有__float__方法的类在运行时被认为是SupportsFloat的虚拟子类,即使__float__方法不返回float

查看这个控制台会话:

>>> import sys
>>> sys.version
'3.9.5 (v3.9.5:0a7dcbdb13, May  3 2021, 13:17:02) \n[Clang 6.0 (clang-600.0.57)]'
>>> c = 3+4j
>>> c.__float__
<method-wrapper '__float__' of complex object at 0x10a16c590>
>>> c.__float__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't convert complex to float

在 Python 3.9 中,complex类型确实有一个__float__方法,但它只是为了引发一个带有明确错误消息的TypeError。如果那个__float__方法有注释,返回类型将是NoReturn,我们在NoReturn中看到过。

但在typeshed上对complex.__float__进行类型提示不会解决这个问题,因为 Python 的运行时通常会忽略类型提示,并且无法访问typeshed存根文件。

继续前面的 Python 3.9 会话:

>>> from typing import SupportsFloat
>>> c = 3+4j
>>> isinstance(c, SupportsFloat)
True
>>> issubclass(complex, SupportsFloat)
True

因此我们有了误导性的结果:针对SupportsFloat的运行时检查表明你可以将complex转换为float,但实际上会引发类型错误。

警告


流畅的 Python 第二版(GPT 重译)(七)(3)https://developer.aliyun.com/article/1484636

相关文章
|
6天前
|
数据采集 存储 人工智能
【Python+微信】【企业微信开发入坑指北】4. 企业微信接入GPT,只需一个URL,自动获取文章总结
【Python+微信】【企业微信开发入坑指北】4. 企业微信接入GPT,只需一个URL,自动获取文章总结
44 0
|
6天前
|
Python
过年了,让GPT用Python给你写个放烟花的程序吧!
过年了,让GPT用Python给你写个放烟花的程序吧!
21 0
|
6天前
|
人工智能 JSON 机器人
【Chat GPT】用 ChatGPT 运行 Python
【Chat GPT】用 ChatGPT 运行 Python
|
6天前
|
机器学习/深度学习 人工智能 自然语言处理
总结几个GPT的超实用之处【附带Python案例】
总结几个GPT的超实用之处【附带Python案例】
|
6天前
|
前端开发 JavaScript 安全
JavaScript 权威指南第七版(GPT 重译)(七)(4)
JavaScript 权威指南第七版(GPT 重译)(七)
29 0
|
6天前
|
前端开发 JavaScript 算法
JavaScript 权威指南第七版(GPT 重译)(七)(3)
JavaScript 权威指南第七版(GPT 重译)(七)
40 0
|
6天前
|
前端开发 JavaScript Unix
JavaScript 权威指南第七版(GPT 重译)(七)(2)
JavaScript 权威指南第七版(GPT 重译)(七)
43 0
|
6天前
|
前端开发 JavaScript 算法
JavaScript 权威指南第七版(GPT 重译)(七)(1)
JavaScript 权威指南第七版(GPT 重译)(七)
70 0
|
6天前
|
存储 前端开发 JavaScript
JavaScript 权威指南第七版(GPT 重译)(六)(4)
JavaScript 权威指南第七版(GPT 重译)(六)
127 3
JavaScript 权威指南第七版(GPT 重译)(六)(4)
|
6天前
|
前端开发 JavaScript API
JavaScript 权威指南第七版(GPT 重译)(六)(3)
JavaScript 权威指南第七版(GPT 重译)(六)
70 4