流畅的 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
,正如最后一行所示。
请注意,我并不是说str
或LineItem
是type
的子类。我要说的是str
和LineItem
是type
的实例。它们都是object
的子类。图 24-2 可能会帮助您面对这个奇怪的现实。
图 24-2。两个图表都是正确的。左边的图表强调str
、type
和LineItem
是object
的子类。右边的图表清楚地表明str
、object
和LineItem
是type
的实例,因为它们都是类。
注意
类object
和type
有一个独特的关系:object
是type
的一个实例,而type
是object
的一个子类。这种关系是“魔法”的:它不能在 Python 中表达,因为任何一个类都必须在另一个类定义之前存在。type
是其自身的实例的事实也是神奇的。
下一个片段显示collections.Iterable
的类是abc.ABCMeta
。请注意,Iterable
是一个抽象类,但ABCMeta
是一个具体类——毕竟,Iterable
是ABCMeta
的一个实例:
>>> 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。Iterable
是object
的子类,也是ABCMeta
的实例。 object
和ABCMeta
都是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__
省略了等于默认值的属性的参数。
MetaBunch
— Bunch
的元类 — 从用户类中声明的类属性生成新类的 __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_cls
(mcs
是一个常见的替代方案)。其余三个参数与直接调用 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__
以显示每个 key
和 value
在设置时的情况。元类将使用一个 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
继承的 NosyDict
的 data
属性。
⑥
在新创建的类中注入一个方法。
⑦
像往常一样,__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.py 和 metalib.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