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

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

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

示例 24-6. initsub/checkedlib.py:Checked类的剩余方法
def __setattr__(self, name: str, value: Any) -> None:  # ①
        if name in self._fields():              # ②
            cls = self.__class__
            descriptor = getattr(cls, name)
            descriptor.__set__(self, value)     # ③
        else:                                   # ④
            self.__flag_unknown_attrs(name)
    def __flag_unknown_attrs(self, *names: str) -> NoReturn:  # ⑤
        plural = 's' if len(names) > 1 else ''
        extra = ', '.join(f'{name!r}' for name in names)
        cls_name = repr(self.__class__.__name__)
        raise AttributeError(f'{cls_name} object has no attribute{plural} {extra}')
    def _asdict(self) -> dict[str, Any]:  # ⑥
        return {
            name: getattr(self, name)
            for name, attr in self.__class__.__dict__.items()
            if isinstance(attr, Field)
        }
    def __repr__(self) -> str:  # ⑦
        kwargs = ', '.join(
            f'{key}={value!r}' for key, value in self._asdict().items()
        )
        return f'{self.__class__.__name__}({kwargs})'

拦截所有尝试设置实例属性。这是为了防止设置未知属性。

如果属性name已知,则获取相应的descriptor

通常我们不需要显式调用描述符__set__。在这种情况下是必要的,因为__setattr__拦截所有尝试在实例上设置属性的尝试,包括在存在覆盖描述符(如Field)的情况下。⁹

否则,属性name是未知的,__flag_unknown_attrs将引发异常。

构建一个有用的错误消息,列出所有意外参数,并引发AttributeError。这是NoReturn特殊类型的一个罕见例子,详见NoReturn

Movie对象的属性创建一个dict。我会将这个方法命名为_as_dict,但我遵循了collections.namedtuple_asdict方法开始的惯例。

实现一个好的__repr__是在这个例子中拥有_asdict的主要原因。

Checked示例说明了在实现__setattr__以阻止实例化后设置任意属性时如何处理覆盖描述符。在这个例子中,实现__setattr__是否值得讨论是有争议的。如果没有,设置movie.director = 'Greta Gerwig'将成功,但director属性不会以任何方式被检查,并且不会出现在__repr__中,也不会包含在_asdict返回的dict中——这两者在示例 24-6 中定义。

record_factory.py(示例 24-2)中,我使用__slots__类属性解决了这个问题。然而,在这种情况下,这种更简单的解决方案是不可行的,如下所述。

为什么__init_subclass__无法配置__slots__

__slots__属性仅在它是传递给type.__new__的类命名空间中的条目之一时才有效。向现有类添加__slots__没有效果。Python 仅在类构建后调用__init_subclass__,此时配置__slots__已经太晚了。类装饰器也无法配置__slots__,因为它甚至比__init_subclass__应用得更晚。我们将在“发生了什么:导入时间与运行时”中探讨这些时间问题。

要在运行时配置 __slots__,您自己的代码必须构建作为 type.__new__ 的最后一个参数传递的类命名空间。为此,您可以编写一个类工厂函数,例如 record_factory.py,或者您可以采取核心选项并实现一个元类。我们将看到如何在 “元类 101” 中动态配置 __slots__

PEP 487 简化了 Python 3.7 中使用 __init_subclass__ 自定义类创建的过程之前,类似的功能必须使用类装饰器来实现。这是下一节的重点。

使用类装饰器增强类

类装饰器是一个可调用对象,类似于函数装饰器:它以装饰的类作为参数,并应返回一个用于替换装饰类的类。类装饰器通常通过属性赋值在装饰类本身后注入更多方法后返回装饰类本身。

选择类装饰器而不是更简单的 __init_subclass__ 最常见的原因可能是为了避免干扰其他类特性,如继承和元类。¹⁰

在本节中,我们将学习 checkeddeco.py,它提供了与 checkedlib.py 相同的服务,但使用了类装饰器。和往常一样,我们将从 checkeddeco.py 中的 doctests 中提取的用法示例开始查看(示例 24-7)。

示例 24-7. checkeddeco.py:创建使用 @checked 装饰的 Movie
>>> @checked
    ... class Movie:
    ...     title: str
    ...     year: int
    ...     box_office: float
    ...
    >>> movie = Movie(title='The Godfather', year=1972, box_office=137)
    >>> movie.title
    'The Godfather'
    >>> movie
    Movie(title='The Godfather', year=1972, box_office=137.0)

示例 24-7 和 示例 24-3 之间唯一的区别是 Movie 类的声明方式:它使用 @checked 装饰而不是继承 Checked。否则,外部行为相同,包括类型验证和默认值分配在 “引入 init_subclass” 中示例 24-3 之后显示的内容。

现在让我们看看 checkeddeco.py 的实现。导入和 Field 类与 checkedlib.py 中的相同,列在 示例 24-4 中。没有其他类,只有 checkeddeco.py 中的函数。

之前在 __init_subclass__ 中实现的逻辑现在是 checked 函数的一部分——类装饰器列在 示例 24-8 中。

示例 24-8. checkeddeco.py:类装饰器
def checked(cls: type) -> type:  # ①
    for name, constructor in _fields(cls).items():    # ②
        setattr(cls, name, Field(name, constructor))  # ③
    cls._fields = classmethod(_fields)  # type: ignore # ④
    instance_methods = (  # ⑤
        __init__,
        __repr__,
        __setattr__,
        _asdict,
        __flag_unknown_attrs,
    )
    for method in instance_methods:  # ⑥
        setattr(cls, method.__name__, method)
    return cls  # ⑦

请记住,类是 type 的实例。这些类型提示强烈暗示这是一个类装饰器:它接受一个类并返回一个类。

_fields 是模块中稍后定义的顶层函数(在 示例 24-9 中)。

Field 描述符实例替换 _fields 返回的每个属性是 __init_subclass__ 在 示例 24-5 中所做的。这里还有更多的工作要做…

_fields 中构建一个类方法,并将其添加到装饰类中。type: ignore 注释是必需的,因为 Mypy 抱怨 type 没有 _fields 属性。

将成为装饰类的实例方法的模块级函数。

将每个 instance_methods 添加到 cls 中。

返回装饰后的 cls,实现类装饰器的基本约定。

checkeddeco.py 中的每个顶层函数都以下划线开头,除了 checked 装饰器。这种命名约定有几个原因是合理的:

  • checkedcheckeddeco.py 模块的公共接口的一部分,但其他函数不是。
  • 示例 24-9 中的函数将被注入到装饰类中,而前导的_减少了与装饰类的用户定义属性和方法的命名冲突的机会。

checkeddeco.py的其余部分列在示例 24-9 中。这些模块级函数与checkedlib.pyChecked类的相应方法具有相同的代码。它们在示例 24-5 和 24-6 中有解释。

请注意,_fields函数在checkeddeco.py中承担了双重职责。它在checked装饰器的第一行中用作常规函数,并且还将被注入为装饰类的类方法。

示例 24-9. checkeddeco.py:要注入到装饰类中的方法
def _fields(cls: type) -> dict[str, type]:
    return get_type_hints(cls)
def __init__(self: Any, **kwargs: Any) -> None:
    for name in self._fields():
        value = kwargs.pop(name, ...)
        setattr(self, name, value)
    if kwargs:
        self.__flag_unknown_attrs(*kwargs)
def __setattr__(self: Any, name: str, value: Any) -> None:
    if name in self._fields():
        cls = self.__class__
        descriptor = getattr(cls, name)
        descriptor.__set__(self, value)
    else:
        self.__flag_unknown_attrs(name)
def __flag_unknown_attrs(self: Any, *names: str) -> NoReturn:
    plural = 's' if len(names) > 1 else ''
    extra = ', '.join(f'{name!r}' for name in names)
    cls_name = repr(self.__class__.__name__)
    raise AttributeError(f'{cls_name} has no attribute{plural} {extra}')
def _asdict(self: Any) -> dict[str, Any]:
    return {
        name: getattr(self, name)
        for name, attr in self.__class__.__dict__.items()
        if isinstance(attr, Field)
    }
def __repr__(self: Any) -> str:
    kwargs = ', '.join(
        f'{key}={value!r}' for key, value in self._asdict().items()
    )
    return f'{self.__class__.__name__}({kwargs})'

checkeddeco.py模块实现了一个简单但可用的类装饰器。Python 的@dataclass做了更多的事情。它支持许多配置选项,向装饰类添加更多方法,处理或警告有关与装饰类中的用户定义方法的冲突,并甚至遍历__mro__以收集在装饰类的超类中声明的用户定义属性。Python 3.9 中dataclasses包的源代码超过 1200 行。

对于元编程类,我们必须意识到 Python 解释器在构建类时何时评估代码块。接下来将介绍这一点。

当发生什么时:导入时间与运行时

Python 程序员谈论“导入时间”与“运行时”,但这些术语并没有严格定义,它们之间存在一个灰色地带。

在导入时,解释器:

  1. 从顶部到底部一次性解析一个*.py*模块的源代码。这是可能发生SyntaxError的时候。
  2. 编译要执行的字节码。
  3. 执行编译模块的顶层代码。

如果本地__pycache__中有最新的*.pyc*文件可用,则解析和编译将被跳过,因为字节码已准备就绪。

尽管解析和编译明显是“导入时间”活动,但在那个时候可能会发生其他事情,因为 Python 中的几乎每个语句都是可执行的,它们可能运行用户代码并可能改变用户程序的状态。

特别是,import语句不仅仅是一个声明,¹¹,而且当模块在进程中首次导入时,它实际上运行模块的所有顶层代码。对同一模块的进一步导入将使用缓存,然后唯一的效果将是将导入的对象绑定到客户模块中的名称。该顶层代码可以执行任何操作,包括典型的“运行时”操作,例如写入日志或连接到数据库。¹²这就是为什么“导入时间”和“运行时”之间的边界模糊:import语句可以触发各种“运行时”行为。反过来,“导入时间”也可能发生在运行时的深处,因为import语句和__import__()内置可以在任何常规函数内使用。

这一切都相当抽象和微妙,所以让我们做一些实验,看看发生了什么。

评估时间实验

考虑一个evaldemo.py脚本,它使用了一个类装饰器、一个描述符和一个基于__init_subclass__的类构建器,所有这些都在builderlib.py模块中定义。这些模块有几个print调用来展示发生了什么。否则,它们不执行任何有用的操作。这些实验的目标是观察这些print调用发生的顺序。

警告

在单个类中同时应用类装饰器和__init_subclass__类构建器很可能是过度设计或绝望的迹象。这种不寻常的组合在这些实验中很有用,可以展示类装饰器和__init_subclass__对类应用的更改的时间。

让我们从builderlib.py开始,分为两部分:示例 24-10 和示例 24-11。

示例 24-10. builderlib.py:模块顶部
print('@ builderlib module start')
class Builder:  # ①
    print('@ Builder body')
    def __init_subclass__(cls):  # ②
        print(f'@ Builder.__init_subclass__({cls!r})')
        def inner_0(self):  # ③
            print(f'@ SuperA.__init_subclass__:inner_0({self!r})')
        cls.method_a = inner_0
    def __init__(self):
        super().__init__()
        print(f'@ Builder.__init__({self!r})')
def deco(cls):  # ④
    print(f'@ deco({cls!r})')
    def inner_1(self):  # ⑤
        print(f'@ deco:inner_1({self!r})')
    cls.method_b = inner_1
    return cls  # ⑥

这是一个类构建器,用于实现…

…一个__init_subclass__方法。

定义一个函数,将在下面的赋值中添加到子类中。

一个类装饰器。

要添加到装饰类的函数。

返回作为参数接收的类。

继续查看示例 24-11 中的builderlib.py

示例 24-11. builderlib.py:模块底部
class Descriptor:  # ①
    print('@ Descriptor body')
    def __init__(self):  # ②
        print(f'@ Descriptor.__init__({self!r})')
    def __set_name__(self, owner, name):  # ③
        args = (self, owner, name)
        print(f'@ Descriptor.__set_name__{args!r}')
    def __set__(self, instance, value):  # ④
        args = (self, instance, value)
        print(f'@ Descriptor.__set__{args!r}')
    def __repr__(self):
        return '<Descriptor instance>'
print('@ builderlib module end')

一个描述符类,用于演示当…

…创建一个描述符实例,当…

__set_name__将在owner类构建期间被调用。

像其他方法一样,这个__set__除了显示其参数外什么也不做。

如果你在 Python 控制台中导入builderlib.py,你会得到以下内容:

>>> import builderlib
@ builderlib module start
@ Builder body
@ Descriptor body
@ builderlib module end

注意builderlib.py打印的行前缀为@

现在让我们转向evaldemo.py,它将触发builderlib.py中的特殊方法(示例 24-12)。

示例 24-12. evaldemo.py:用于实验builderlib.py的脚本
#!/usr/bin/env python3
from builderlib import Builder, deco, Descriptor
print('# evaldemo module start')
@deco  # ①
class Klass(Builder):  # ②
    print('# Klass body')
    attr = Descriptor()  # ③
    def __init__(self):
        super().__init__()
        print(f'# Klass.__init__({self!r})')
    def __repr__(self):
        return '<Klass instance>'
def main():  # ④
    obj = Klass()
    obj.method_a()
    obj.method_b()
    obj.attr = 999
if __name__ == '__main__':
    main()
print('# evaldemo module end')

应用一个装饰器。

子类化Builder以触发其__init_subclass__

实例化描述符。

这只会在模块作为主程序运行时调用。

evaldemo.py中的print调用显示了#前缀。如果你再次打开控制台并导入evaldemo.py,示例 24-13 就是输出结果。

示例 24-13. evaldemo.py的控制台实验
>>> import evaldemo
@ builderlib module start # ①
@ Builder body @ Descriptor body @ builderlib module end # evaldemo module start # Klass body # ②
@ Descriptor.__init__(<Descriptor instance>) # ③
@ Descriptor.__set_name__(<Descriptor instance>,
 <class 'evaldemo.Klass'>, 'attr') # ④
@ Builder.__init_subclass__(<class 'evaldemo.Klass'>) # ⑤
@ deco(<class 'evaldemo.Klass'>) # ⑥
# evaldemo module end

前四行是from builderlib import…的结果。如果你在之前的实验后没有关闭控制台,它们将不会出现,因为builderlib.py已经被加载。

这表明 Python 开始读取Klass的主体。此时,类对象还不存在。

描述符实例被创建并绑定到命名空间中的attr,Python 将把它传递给默认的类对象构造函数:type.__new__

此时,Python 内置的type.__new__已经创建了Klass对象,并在每个提供该方法的描述符类的描述符实例上调用__set_name__,将Klass作为owner参数传递。

然后type.__new__Klass的超类上调用__init_subclass__,将Klass作为唯一参数传递。

type.__new__返回类对象时,Python 会应用装饰器。在这个例子中,deco返回的类会绑定到模块命名空间中的Klass

type.__new__的实现是用 C 语言编写的。我刚描述的行为在 Python 的“数据模型”参考中的“创建类对象”部分有文档记录。

请注意,evaldemo.pymain()函数(示例 24-12)没有在控制台会话中执行(示例 24-13),因此没有创建Klass的实例。我们看到的所有操作都是由“import time”操作触发的:导入builderlib和定义Klass

如果你将evaldemo.py作为脚本运行,你将看到与示例 24-13 相同的输出,但在最后之前会有额外的行。额外的行是运行main()(示例 24-14)的结果。

示例 24-14。作为程序运行evaldemo.py
$ ./evaldemo.py
[... 9 lines omitted ...]
@ deco(<class '__main__.Klass'>)  # ①
@ Builder.__init__(<Klass instance>)  # ②
# Klass.__init__(<Klass instance>)
@ SuperA.__init_subclass__:inner_0(<Klass instance>)  # ③
@ deco:inner_1(<Klass instance>)  # ④
@ Descriptor.__set__(<Descriptor instance>, <Klass instance>, 999)  # ⑤
# evaldemo module end

前 10 行(包括这一行)与示例 24-13 中显示的相同。

Klass.__init__中由super().__init__()触发。

main中由obj.method_a()触发;method_a是由SuperA.__init_subclass__注入的。

main中由obj.method_b()触发;method_b是由deco注入的。

main中由obj.attr = 999触发。

具有__init_subclass__和类装饰器的基类是强大的工具,但它们仅限于使用type.__new__在内部构建的类。在需要调整传递给type.__new__的参数的罕见情况下,您需要一个元类。这是本章和本书的最终目的地。


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

相关文章
|
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 重译)(七)
39 0
|
6天前
|
前端开发 JavaScript Unix
JavaScript 权威指南第七版(GPT 重译)(七)(2)
JavaScript 权威指南第七版(GPT 重译)(七)
43 0
|
6天前
|
前端开发 JavaScript 算法
JavaScript 权威指南第七版(GPT 重译)(七)(1)
JavaScript 权威指南第七版(GPT 重译)(七)
69 0
|
6天前
|
存储 前端开发 JavaScript
JavaScript 权威指南第七版(GPT 重译)(六)(4)
JavaScript 权威指南第七版(GPT 重译)(六)
127 3
JavaScript 权威指南第七版(GPT 重译)(六)(4)
|
6天前
|
前端开发 JavaScript API
JavaScript 权威指南第七版(GPT 重译)(六)(3)
JavaScript 权威指南第七版(GPT 重译)(六)
68 4