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

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

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

元类 101

[元类]比 99%的用户应该担心的更深奥。如果你想知道是否需要它们,那就不需要(真正需要它们的人确信自己需要它们,并不需要解释为什么)。

Tim Peters,Timsort 算法的发明者和多产的 Python 贡献者¹³

元类是一个类工厂。与示例 24-2 中的record_factory相比,元类是作为一个类编写的。换句话说,元类是一个其实例是类的类。图 24-1 使用 Mills & Gizmos 符号表示了一个元类:一个生产另一个元类的工厂。

图 24-1。元类是一个构建类的类。

考虑 Python 对象模型:类是对象,因此每个类必须是另一个类的实例。默认情况下,Python 类是type的实例。换句话说,type是大多数内置和用户定义类的元类:

>>> str.__class__
<class 'type'>
>>> from bulkfood_v5 import LineItem
>>> LineItem.__class__
<class 'type'>
>>> type.__class__
<class 'type'>

为了避免无限递归,type的类是type,正如最后一行所示。

请注意,我并不是说strLineItemtype的子类。我要说的是strLineItemtype的实例。它们都是object的子类。图 24-2 可能会帮助您面对这个奇怪的现实。

图 24-2。两个图表都是正确的。左边的图表强调strtypeLineItemobject的子类。右边的图表清楚地表明strobjectLineItemtype的实例,因为它们都是类。
注意

objecttype有一个独特的关系:objecttype的一个实例,而typeobject的一个子类。这种关系是“魔法”的:它不能在 Python 中表达,因为任何一个类都必须在另一个类定义之前存在。type是其自身的实例的事实也是神奇的。

下一个片段显示collections.Iterable的类是abc.ABCMeta。请注意,Iterable是一个抽象类,但ABCMeta是一个具体类——毕竟,IterableABCMeta的一个实例:

>>> from collections.abc import Iterable
>>> Iterable.__class__
<class 'abc.ABCMeta'>
>>> import abc
>>> from abc import ABCMeta
>>> ABCMeta.__class__
<class 'type'>

最终,ABCMeta的类也是type。 每个类都是type的实例,直接或间接,但只有元类也是type的子类。 这是理解元类最重要的关系:元类(例如ABCMeta)从type继承了构造类的能力。 图 24-3 说明了这种关键关系。

图 24-3。Iterableobject的子类,也是ABCMeta的实例。 objectABCMeta都是type的实例,但这里的关键关系是ABCMeta也是type的子类,因为ABCMeta是一个元类。 在这个图表中,Iterable是唯一的抽象类。

这里的重要要点是元类是type的子类,这就是使它们作为类工厂运作的原因。 通过实现特殊方法,元类可以定制其实例,如下一节所示。

元类如何定制类

要使用元类,了解__new__如何在任何类上运行至关重要。 这在“使用 new 进行灵活的对象创建”中讨论过。

当元类即将创建一个新实例(即类)时,类似的机制发生在“元”级别。 考虑这个声明:

class Klass(SuperKlass, metaclass=MetaKlass):
    x = 42
    def __init__(self, y):
        self.y = y

要处理该class语句,Python 使用这些参数调用MetaKlass.__new__

meta_cls

元类本身(MetaKlass),因为__new__作为类方法运行。

cls_name

字符串Klass

bases

单元素元组(SuperKlass,),在多重继承的情况下有更多元素。

cls_dict

一个类似于:

{x: 42, `__init__`: <function __init__ at 0x1009c4040>}

当您实现MetaKlass.__new__时,您可以检查并更改这些参数,然后将它们传递给super().__new__,后者最终将调用type.__new__来创建新的类对象。

super().__new__返回后,您还可以对新创建的类进行进一步处理,然后将其返回给 Python。 然后,Python 调用SuperKlass.__init_subclass__,传递您创建的类,然后对其应用类装饰器(如果存在)。 最后,Python 将类对象绑定到其名称在周围的命名空间中 - 通常是模块的全局命名空间,如果class语句是顶级语句。

元类__new__中最常见的处理是向cls_dict中添加或替换项目 - 代表正在构建的类的命名空间的映射。 例如,在调用super().__new__之前,您可以通过向cls_dict添加函数来向正在构建的类中注入方法。 但是,请注意,添加方法也可以在构建类之后完成,这就是为什么我们能够使用__init_subclass__或类装饰器来完成的原因。

type.__new__运行之前,您必须向cls_dict添加的一个属性是__slots__,如“为什么 init_subclass 无法配置 slots”中讨论的那样。 元类的__new__方法是配置__slots__的理想位置。 下一节将展示如何做到这一点。

一个很好的元类示例

这里介绍的MetaBunch元类是Python in a Nutshell,第 3 版第四章中最后一个示例的变体,作者是 Alex Martelli,Anna Ravenscroft 和 Steve Holden,编写以在 Python 2.7 和 3.5 上运行。 假设是 Python 3.6 或更高版本,我能够进一步简化代码。

首先,让我们看看Bunch基类提供了什么:

>>> class Point(Bunch):
 ...     x = 0.0
 ...     y = 0.0
 ...     color = 'gray'
 ...
 >>> Point(x=1.2, y=3, color='green')
 Point(x=1.2, y=3, color='green')
 >>> p = Point()
 >>> p.x, p.y, p.color
 (0.0, 0.0, 'gray')
 >>> p
 Point()

请记住,Checked根据类变量类型提示为子类中的Field描述符分配名称,这些描述符实际上不会成为类的属性,因为它们没有值。

另一方面,Bunch 的子类使用具有值的实际类属性,然后这些值成为实例属性的默认值。生成的 __repr__ 省略了等于默认值的属性的参数。

MetaBunchBunch 的元类 — 从用户类中声明的类属性生成新类的 __slots__。这阻止了未声明属性的实例化和后续赋值:

>>> Point(x=1, y=2, z=3)
 Traceback (most recent call last):
 ...
 AttributeError: No slots left for: 'z'
 >>> p = Point(x=21)
 >>> p.y = 42
 >>> p
 Point(x=21, y=42)
 >>> p.flavor = 'banana'
 Traceback (most recent call last):
 ...
 AttributeError: 'Point' object has no attribute 'flavor'

现在让我们深入研究 示例 24-15 中 MetaBunch 的优雅代码。

示例 24-15. metabunch/from3.6/bunch.py:MetaBunch 元类和 Bunch
class MetaBunch(type):  # ①
    def __new__(meta_cls, cls_name, bases, cls_dict):  # ②
        defaults = {}  # ③
        def __init__(self, **kwargs):  # ④
            for name, default in defaults.items():  # ⑤
                setattr(self, name, kwargs.pop(name, default))
            if kwargs:  # ⑥
                extra = ', '.join(kwargs)
                raise AttributeError(f'No slots left for: {extra!r}')
        def __repr__(self):  # ⑦
            rep = ', '.join(f'{name}={value!r}'
                            for name, default in defaults.items()
                            if (value := getattr(self, name)) != default)
            return f'{cls_name}({rep})'
        new_dict = dict(__slots__=[], __init__=__init__, __repr__=__repr__)  # ⑧
        for name, value in cls_dict.items():  # ⑨
            if name.startswith('__') and name.endswith('__'):  # ⑩
                if name in new_dict:
                    raise AttributeError(f"Can't set {name!r} in {cls_name!r}")
                new_dict[name] = value
            else:  ⑪
                new_dict['__slots__'].append(name)
                defaults[name] = value
        return super().__new__(meta_cls, cls_name, bases, new_dict)  ⑫
class Bunch(metaclass=MetaBunch):  ⑬
    pass

要创建一个新的元类,继承自 type

__new__ 作为一个类方法工作,但类是一个元类,所以我喜欢将第一个参数命名为 meta_clsmcs 是一个常见的替代方案)。其余三个参数与直接调用 type() 创建类的三参数签名相同。

defaults 将保存属性名称和它们的默认值的映射。

这将被注入到新类中。

读取 defaults 并使用从 kwargs 弹出的值或默认值设置相应的实例属性。

如果 kwargs 中仍有任何项,这意味着没有剩余的插槽可以放置它们。我们认为快速失败是最佳实践,因此我们不希望悄悄地忽略额外的项。一个快速有效的解决方案是从 kwargs 中弹出一项并尝试在实例上设置它,故意触发 AttributeError

__repr__ 返回一个看起来像构造函数调用的字符串 — 例如,Point(x=3),省略了具有默认值的关键字参数。

初始化新类的命名空间。

遍历用户类的命名空间。

如果找到双下划线 name,则将项目复制到新类命名空间,除非它已经存在。这可以防止用户覆盖由 Python 设置的 __init____repr__ 和其他属性,如 __qualname____module__

如果不是双下划线 name,则追加到 __slots__ 并将其 value 保存在 defaults 中。

构建并返回新类。

提供一个基类,这样用户就不需要看到 MetaBunch

MetaBunch 起作用是因为它能够在调用 super().__new__ 之前配置 __slots__ 以构建最终类。通常在元编程时,理解操作的顺序至关重要。让我们进行另一个评估时间实验,这次使用元类。

元类评估时间实验

这是 “评估时间实验” 的一个变体,加入了一个元类。builderlib.py 模块与之前相同,但主脚本现在是 evaldemo_meta.py,列在 示例 24-16 中。

示例 24-16. evaldemo_meta.py:尝试使用元类进行实验
#!/usr/bin/env python3
from builderlib import Builder, deco, Descriptor
from metalib import MetaKlass  # ①
print('# evaldemo_meta module start')
@deco
class Klass(Builder, metaclass=MetaKlass):  # ②
    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.method_c()  # ③
    obj.attr = 999
if __name__ == '__main__':
    main()
print('# evaldemo_meta module end')

metalib.py 导入 MetaKlass,我们将在 示例 24-18 中看到。

Klass 声明为 Builder 的子类和 MetaKlass 的实例。

此方法是由 MetaKlass.__new__ 注入的,我们将会看到。

警告

为了科学研究,示例 24-16 违背一切理性,将三种不同的元编程技术应用于 Klass:一个装饰器,一个使用 __init_subclass__ 的基类,以及一个自定义元类。如果你在生产代码中这样做,请不要责怪我。再次强调,目标是观察这三种技术干扰类构建过程的顺序。

与之前的评估时间实验一样,这个例子除了打印显示执行流程的消息外什么也不做。示例 24-17 展示了 metalib.py 顶部部分的代码—其余部分在 示例 24-18 中。

示例 24-17. metalib.py:NosyDict
print('% metalib module start')
import collections
class NosyDict(collections.UserDict):
    def __setitem__(self, key, value):
        args = (self, key, value)
        print(f'% NosyDict.__setitem__{args!r}')
        super().__setitem__(key, value)
    def __repr__(self):
        return '<NosyDict instance>'

我编写了 NosyDict 类来重写 __setitem__ 以显示每个 keyvalue 在设置时的情况。元类将使用一个 NosyDict 实例来保存正在构建的类的命名空间,揭示 Python 更多的内部工作原理。

metalib.py 的主要吸引力在于 示例 24-18 中的元类。它实现了 __prepare__ 特殊方法,这是 Python 仅在元类上调用的类方法。__prepare__ 方法提供了影响创建新类过程的最早机会。

提示

在编写元类时,我发现采用这种特殊方法参数的命名约定很有用:

  • 对于实例方法,使用 cls 而不是 self,因为实例是一个类。
  • 对于类方法,使用 meta_cls 而不是 cls,因为类是一个元类。请记住,__new__ 表现为类方法,即使没有 @classmethod 装饰器。
示例 24-18. metalib.py:MetaKlass
class MetaKlass(type):
    print('% MetaKlass body')
    @classmethod  # ①
    def __prepare__(meta_cls, cls_name, bases):  # ②
        args = (meta_cls, cls_name, bases)
        print(f'% MetaKlass.__prepare__{args!r}')
        return NosyDict()  # ③
    def __new__(meta_cls, cls_name, bases, cls_dict):  # ④
        args = (meta_cls, cls_name, bases, cls_dict)
        print(f'% MetaKlass.__new__{args!r}')
        def inner_2(self):
            print(f'% MetaKlass.__new__:inner_2({self!r})')
        cls = super().__new__(meta_cls, cls_name, bases, cls_dict.data)  # ⑤
        cls.method_c = inner_2  # ⑥
        return cls  # ⑦
    def __repr__(cls):  # ⑧
        cls_name = cls.__name__
        return f"<class {cls_name!r} built by MetaKlass>"
print('% metalib module end')

__prepare__ 应该声明为类方法。它不是实例方法,因为在 Python 调用 __prepare__ 时正在构建的类还不存在。

Python 调用元类的 __prepare__ 来获取一个映射,用于保存正在构建的类的命名空间。

返回 NosyDict 实例以用作命名空间。

cls_dict 是由 __prepare__ 返回的 NosyDict 实例。

type.__new__ 要求最后一个参数是一个真实的 dict,所以我给了它从 UserDict 继承的 NosyDictdata 属性。

在新创建的类中注入一个方法。

像往常一样,__new__ 必须返回刚刚创建的对象—在这种情况下是新类。

在元类上定义 __repr__ 允许自定义类对象的 repr()

在 Python 3.6 之前,__prepare__ 的主要用途是提供一个 OrderedDict 来保存正在构建的类的属性,以便元类 __new__ 可以按照用户类定义源代码中的顺序处理这些属性。现在 dict 保留插入顺序,__prepare__ 很少被需要。你将在 “使用 prepare 进行元类黑客” 中看到它的创造性用法。

在 Python 控制台中导入 metalib.py 并不是很令人兴奋。请注意使用 % 作为此模块输出的行的前缀:

>>> import metalib
% metalib module start
% MetaKlass body
% metalib module end

如果导入 evaldemo_meta.py,会发生很多事情,正如你在 示例 24-19 中所看到的。

示例 24-19. 使用 evaldemo_meta.py 的控制台实验
>>> import evaldemo_meta
@ builderlib module start @ Builder body @ Descriptor body @ builderlib module end % metalib module start % MetaKlass body % metalib module end # evaldemo_meta module start # ①
% MetaKlass.__prepare__(<class 'metalib.MetaKlass'>, 'Klass', # ②
 (<class 'builderlib.Builder'>,)) % NosyDict.__setitem__(<NosyDict instance>, '__module__', 'evaldemo_meta') # ③
% NosyDict.__setitem__(<NosyDict instance>, '__qualname__', 'Klass') # Klass body @ Descriptor.__init__(<Descriptor instance>) # ④
% NosyDict.__setitem__(<NosyDict instance>, 'attr', <Descriptor instance>) # ⑤
% NosyDict.__setitem__(<NosyDict instance>, '__init__',
 <function Klass.__init__ at …>) # ⑥
% NosyDict.__setitem__(<NosyDict instance>, '__repr__',
 <function Klass.__repr__ at …>) % NosyDict.__setitem__(<NosyDict instance>, '__classcell__', <cell at …: empty>) % MetaKlass.__new__(<class 'metalib.MetaKlass'>, 'Klass',
 (<class 'builderlib.Builder'>,), <NosyDict instance>) # ⑦
@ Descriptor.__set_name__(<Descriptor instance>,
 <class 'Klass' built by MetaKlass>, 'attr') # ⑧
@ Builder.__init_subclass__(<class 'Klass' built by MetaKlass>) @ deco(<class 'Klass' built by MetaKlass>) # evaldemo_meta module end

在此之前的行是导入 builderlib.pymetalib.py 的结果。

Python 调用 __prepare__ 来开始处理 class 语句。

在解析类体之前,Python 将__module____qualname__条目添加到正在构建的类的命名空间中。

创建描述符实例…

…并绑定到类命名空间中的attr

__init____repr__方法被定义并添加到命名空间中。

Python 完成处理类体后,调用MetaKlass.__new__

__set_name____init_subclass__和装饰器按照这个顺序被调用,在元类的__new__方法返回新构造的类之后。

如果将evaldemo_meta.py作为脚本运行,将调用main(),并会发生一些其他事情(示例 24-20)。

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

相关文章
|
4月前
|
存储 NoSQL 索引
Python 金融编程第二版(GPT 重译)(一)(4)
Python 金融编程第二版(GPT 重译)(一)
56 2
|
3月前
|
人工智能 API Python
Openai python调用gpt测试代码
这篇文章提供了使用OpenAI的Python库调用GPT-4模型进行聊天的测试代码示例,包括如何设置API密钥、发送消息并接收AI回复。
|
4月前
|
存储 算法 数据可视化
Python 金融编程第二版(GPT 重译)(一)(1)
Python 金融编程第二版(GPT 重译)(一)
82 1
|
4月前
|
数据库 开发者 Python
异步编程不再难!Python asyncio库实战,让你的代码流畅如丝!
【7月更文挑战第10天】Python的asyncio库简化了异步编程,提高并发处理能力。async定义异步函数,await等待结果而不阻塞。示例展示了如何用aiohttp进行异步HTTP请求及使用asyncio.gather并发处理任务。通过asyncio,Python开发者能更高效地处理网络I/O和其他并发场景。开始探索异步编程,提升代码效率!**
56 0
|
4月前
|
存储 算法 数据建模
Python 金融编程第二版(GPT 重译)(一)(5)
Python 金融编程第二版(GPT 重译)(一)
34 0
|
4月前
|
安全 Shell 网络安全
Python 金融编程第二版(GPT 重译)(一)(3)
Python 金融编程第二版(GPT 重译)(一)
23 0
|
4月前
|
算法 Linux Docker
Python 金融编程第二版(GPT 重译)(一)(2)
Python 金融编程第二版(GPT 重译)(一)
44 0
|
11天前
|
安全 数据处理 开发者
Python中的多线程编程:从入门到精通
本文将深入探讨Python中的多线程编程,包括其基本原理、应用场景、实现方法以及常见问题和解决方案。通过本文的学习,读者将对Python多线程编程有一个全面的认识,能够在实际项目中灵活运用。
|
5天前
|
设计模式 开发者 Python
Python编程中的设计模式:工厂方法模式###
本文深入浅出地探讨了Python编程中的一种重要设计模式——工厂方法模式。通过具体案例和代码示例,我们将了解工厂方法模式的定义、应用场景、实现步骤以及其优势与潜在缺点。无论你是Python新手还是有经验的开发者,都能从本文中获得关于如何在实际项目中有效应用工厂方法模式的启发。 ###
|
11天前
|
弹性计算 安全 小程序
编程之美:Python让你领略浪漫星空下的流星雨奇观
这段代码使用 Python 的 `turtle` 库实现了一个流星雨动画。程序通过创建 `Meteor` 类来生成具有随机属性的流星,包括大小、颜色、位置和速度。在无限循环中,流星不断移动并重新绘制,营造出流星雨的效果。环境需求为 Python 3.11.4 和 PyCharm 2023.2.5。

热门文章

最新文章