文章目录
- 先说结论
- 1. metaclass 的作用
- 2. 主要的执行过程
- 1. `metaclass.__new__`
- 2. `metaclass.__call__`
- 关于 `metaclass.__init__`
- 3. `metaclass.__prepare__`
- 4. 自动创建 `__slots__` 属性
- 4.1 metaclass 的接口类
- 4.2 metaclass conflict
- 5. Class metaprogramming
先说结论
1. metaclass 的作用
metaclass 的作用是:对新创建的类 new_class 实现一些定制 customization 功能。例如让 new_class 实现自动增加 __slots__
的功能。
Python 默认的 metaclass 是 type,除了把 type 用于查询类型,type 还可以可以创建 class。而自定义的 metaclass 则继承 type,因此自定义的 metaclass 也能在创建 new_class 时,对 new_class 进行定制。
2. 主要的执行过程
在创建一个新的类 new_class 时,主要的执行过程包括如下 6 步(下面用 new_class 作为新类的名字) :
- 解析 MRO。
- 确定 metaclass。
- 准备好命名空间 namespace。
3.1 执行metaclass.__prepare__
方法,默认返回的一个空字典 namespace_dict 作为命名空间。
3.2 把__qualname__ 和 __module__
放入到 namespace_dict 中。 - 执行 new_class 的 body 部分,然后再把 new_class 的所有属性,一起放入到 namespace_dict 中。
- 执行
metaclass.__new__
方法。注意它会被__init_subclass__
打断,分成 2 部分代码执行。
5.1 执行metaclass.__new__
中的命令,直到super().__new__()
为止。
5.2 执行基类的__init_subclass__
方法。
5.3 然后再执行super().__new__()
之后的代码。
5.4 如果metaclass.__new__
方法返回了一个 class 对象,会自动执行metaclass.__init__
方法。 - 将新建的 class 对象和名字 new_class 进行绑定。
对 class 的定制操作,主要发生在上面的第 3、4、5 步骤。
使用 metaclass 时的详细执行过程,可以参看 Python 官网: https://docs.python.org/3/reference/datamodel.html#metaclasses
下面进行详细讨论。
下面的内容涉及到 type(),super() 和 __mro__,__slots__,__call__,__init_subclass__ 等。
读者需要先了解它们的基本用法之后,才能方便地阅读本篇的内容。
关于 super() 和 mro,可以参看我的另一篇文章:
《Python 的 super 函数, mro 和多继承》https://blog.csdn.net/drin201312/article/details/137398779
1. metaclass.__new__
在创建新的 class 时,如果用到 metaclass ,则会调用 metaclass.__new__
方法。
metaclass.__new__
会有 4 个固定参数 meta_cls, new_cls_name, bases, namespace_dict,以及另外一个关键字参数 kwargs。
下图示例展示了 metaclass.__new__
的用法,可以查看这 5 个参数的具体内容。
建议把 metaclass.__new__
的第一个参数写为 meta_cls 或是 mcs 。这是因为, metaclass.__new__
是一个 static method,它的第一个参数是 metaclass,写成 meta_cls 更能表达其含义。
运行结果如下图,在 metaclass.__new__
方法中,注意下面 5 个特点:
- meta_cls 是当前的 metaclass,在此例中就是 MyMeta。
- new_cls_name 则是即将被创建出来的类 NewCls 。
- bases 是元祖,列出所有的基类 。
- namespace_dict 是用 class 关键字定义 NewCls 时,所有的属性和方法。
- 用 super 创建新类时,必须区分是否有基类的两种情况。
2. metaclass.__call__
metaclass 的一个特点是:每次对类 new_class 创建一个 instance,都会执行 metaclass.__call__
方法。
其中的逻辑如下:
- 对一个对象 Foo 使用小括号 () 进行调用时,即执行 Foo() 时,就会执行
Foo.__class__ 的 __call__
方法。 - 如果这个 Foo 是一个 class,
Foo.__class__
就是 Foo 的 metaclass。 - 而执行 Foo() 就是在创建 Foo 的 instance。
因此,创建一个 class 的 instance 时,就会执行该 class 对应的 metaclass.__call__
方法。
下图的 Singleton 就是利用了这个特性,使得只能给 Singleton 创建唯一的一个 instance。该示例参考了第三版 《Python Cookbook》中的 Singleton,进行了一些修改,以便使其更易理解。
关于 metaclass.__init__
如果 metaclass.__new__
运行并返回一个 instance ,则会自动调用 metaclass.__init__
。除了第一个参数,其它参数和 metaclass.__new__
一样。并且 metaclass.__init__
是在基类的 __init_subclass__
方法之后执行。
但是对于 metaclass 来说,一般可以不使用 metaclass.__init__
。初始化的工作,可以放在 metaclass.__new__
中进行,正如上图例子的 SingletonMeta 所示。具体原因是:
- metaclass 有
metaclass.__new__
方法,只要super().__new__
返回了新的 class ,就可立刻对这个 class 进行初始化。 - 而普通的 class,因为一般不会单独对其创建
__new__
方法,所以只能把初始化的工作放到__init__
中。
3. metaclass.__prepare__
__prepare__
只对 metaclass 有效。
现在已经不太需要使用 metaclass.__prepare__
了。原因是:
在 Python 3.6 之前,字典是没有顺序的。metaclass.__prepare__
中使用 OrderedDict ,使得类属性保持一个固定的顺序,这个顺序就是定义 class 时的顺序。然后 metaclass.__new__
可以按这个固定的顺序来处理这些属性。
但是 Python 3.6 之后的字典已经是有顺序的了,所以不再需要使用 metaclass.__prepare__
对属性进行排序。
下图是 David Beazley 在《Python Distilled》中的例子,它展示了 metaclass.__prepare__
的一种用法:用它可以检查 class 中的属性是否有重名。我对这个例子做了一些修改和说明,以便于理解。
这个示例的原理是:在创建新的 class 时,metaclass.__prepare__
会返回一个空字典,而新的 class 的所有属性,都要被放入这个字典中。因此可以对这个字典进行定制,用它检查属性是否重名。
上图的运行结果如下。
4. 自动创建 __slots__
属性
可以使用 metaclass 进行一种定制 customization:让新建的 new_class 自动创建 __slots__
属性。
如果要创建 __slots__
属性,则必须在生成 class 对象之前,也就是 type.__new__
之前,否则 __slots__
无效。
因此,在 metaclass.__new__
方法中,必须在 super().__new__
之前就创建好 __slots__
。
同理,__init_subclass__
和 class decorator 都无法设置 __slots__
,因为 __init_subclass__
和 class decorator 都是在创建好 class 对象之后才起作用的。
自动创建 __slots__
的示例如下,3 个主要步骤是:
- 先获得预先定义好的
__slots__
。 - 把
__init__
中的参数添加到__slots__
中。 - 把两个来源的
__slots__
求并集,放入 namespace_dict 中。
import inspect
class AutoSlotMeta(type):
"""该 metaclass 的作用是把 __init__ 中的参数,自动添加到 __slots__ 属性中。 """
def __new__(meta_cls, new_cls_name, bases, namespace_dict, **kwargs): # noqa
slots = namespace_dict.pop('__slots__', {}) # 1. 先获得预先定义好的 __slots__。
# 2. 然后把 __init__ 中的参数添加到 __slots__ 中。
if '__init__' in namespace_dict:
# 把 __init__ 方法的参数转换为 Signature 对象。
sig = inspect.signature(namespace_dict['__init__'])
# 将 Signature.parameters 转换为一个元祖,只保留参数的名字,去掉参数的默认值。
slots_from_init = tuple(sig.parameters)[1:] # 去掉第 0 位的 self 参数。
slots = set(slots) # 转换为集合再求并集。
slots |= set(slots_from_init) # 3. 把两个来源的 __slots__ 求并集,放入 namespace_dict 中。
namespace_dict['__slots__'] = tuple(slots) # 重新创建 __slots__
print(f'{new_cls_name= }, {namespace_dict["__slots__"]=}')
if bases: # 当有基类时,kwargs 会被传递给基类的 __init_subclass__ 方法。
new_class = super().__new__(meta_cls, new_cls_name, bases, namespace_dict, **kwargs)
else: # 没有基类时,不应该传入 kwargs。因为 type.__init_subclass__ 不接收多余的关键字参数。
new_class = super().__new__(meta_cls, new_cls_name, bases, namespace_dict)
return new_class
class Parent:
__slots__ = () # 为了子类的 __slots__ 起作用,基类必须有 __slots__ 属性。
# 在使用 metaclass 的接口类时,如果还要继承基类,则应该用接口类来继承基类,发生 metaclass conflict,如下所示。
class SlotInterface(Parent, metaclass=AutoSlotMeta):
pass
# 如果有基类,则基类也必须有 __slots__ 属性。否则子类的 __slots__ 不起作用,导致子类可以随意创建属性。
class DemoSlot(SlotInterface):
__slots__ = 'foo', # 如果需要把其它属性放入 __slots__,可以预先定义。
def __init__(self, bar=None):
pass
使用这个的 AutoSlotMeta 效果如下图。
这个自动创建 __slots__
的例子参考了 《Python Distilled》的 SlotMeta,并做了一些改进。
《Fluent Python》中 24-15 例子的 MetaBunch ,以及 Caleb Hattingh 的 autoslot 库,都可以自动创建 __slots__
,也可以参考。autoslot 库的地址:https://github.com/cjrh/autoslot
在这个例子中,提到了 metaclass 的接口类和 metaclass conflict,下面略作介绍。
4.1 metaclass 的接口类
使用 metaclass 时,一种常见做法是使用接口类。具体做法是:创建 metaclass,然后创建一个普通的 class Interface 作为接口 class ,这个 Interface 则直接使用 metaclass。上面例子的 SlotInterface 就是接口类。
这样做既可以获得 metaclass 的功能,又可以尽量保持接口稳定,并且把 metaclass 作为内部细节 implementation detail,进行了隐藏。
接口稳定是指:用户创建新类时始终继承 Interface ,不需要更改。而开发者如果创建了新的 metaclass,只需要修改 Interface ,让 Interface 使用最新的 metaclass 即可。
另外注意,在使用 metaclass 的接口类时,如果还要继承基类,则应该用接口类来继承基类,即写成 class SlotInterface(Parent, metaclass=AutoSlotMeta) 的形式。
4.2 metaclass conflict
使用 metaclass 时,有时会产生 metaclass conflict 报错: typeError:metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
。
如果同时满足下面 3 个条件,就会产生 metaclass conflict:
- class 同时设置了基类和 metaclass Foo 。
- 基类也有 metaclass,假设为 BaseMeta.
- 如果 metaclass Foo 不是 BaseMeta 的子类,则会发生 metaclass conflict。
在 AutoSlotMeta 这个例子中,如果用新类 DemoSlot 直接继承基类 Parent,就会引发报错 metaclass conflict 。
5. Class metaprogramming
最后说一下 metaprogramming 和 class metaprogramming 这两个术语。
-
metaprogramming 和普通的 programming 相对。普通 programming 编写普通的 class 和 function 等,这些 class 和 function 用于实现直接的功能,比如计算求和,求平均值等功能。
-
而
metaprogramming 是指编写一些 meta code,这些 meta code 用于创建或修改其它 class,function 。meta code 本身并不实现直接的功能
。
常见的 metaprogramming 包括:装饰器 decorator,工厂函数 factory function,以及 metaclass 等,因为它们都是用于创建或修改其它 function 或 class 。
(meta code 是我自己编的一个词语。主要目的是把它和普通的 code 区别开,表示它的作用和普通 code 不同) -
class metaprogramming 则是指创建的 meta code 专用于对其它 class 进行创建和修改。class decorator ,
__init_subclass__
, class factory 和 metaclass 都属于 class metaprogramming 。
—————————— 本文结束 ——————————