Python 的 metaclass


先说结论

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 作为新类的名字) :

  1. 解析 MRO。
  2. 确定 metaclass。
  3. 准备好命名空间 namespace。
    3.1 执行 metaclass.__prepare__ 方法,默认返回的一个空字典 namespace_dict 作为命名空间。
    3.2 把 __qualname__ 和 __module__ 放入到 namespace_dict 中。
  4. 执行 new_class 的 body 部分,然后再把 new_class 的所有属性,一起放入到 namespace_dict 中。
  5. 执行 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__ 方法。
  6. 将新建的 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 个特点:

  1. meta_cls 是当前的 metaclass,在此例中就是 MyMeta。
  2. new_cls_name 则是即将被创建出来的类 NewCls 。
  3. bases 是元祖,列出所有的基类 。
  4. namespace_dict 是用 class 关键字定义 NewCls 时,所有的属性和方法。
  5. 用 super 创建新类时,必须区分是否有基类的两种情况。
    在这里插入图片描述

2. metaclass.__call__

metaclass 的一个特点是:每次对类 new_class 创建一个 instance,都会执行 metaclass.__call__ 方法。
其中的逻辑如下:

  1. 对一个对象 Foo 使用小括号 () 进行调用时,即执行 Foo() 时,就会执行 Foo.__class__ 的 __call__ 方法。
  2. 如果这个 Foo 是一个 class, Foo.__class__ 就是 Foo 的 metaclass。
  3. 而执行 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 所示。具体原因是:

  1. metaclass 有 metaclass.__new__ 方法,只要 super().__new__ 返回了新的 class ,就可立刻对这个 class 进行初始化。
  2. 而普通的 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 个主要步骤是:

  1. 先获得预先定义好的 __slots__
  2. __init__ 中的参数添加到 __slots__ 中。
  3. 把两个来源的 __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:

  1. class 同时设置了基类和 metaclass Foo 。
  2. 基类也有 metaclass,假设为 BaseMeta.
  3. 如果 metaclass Foo 不是 BaseMeta 的子类,则会发生 metaclass conflict。

在 AutoSlotMeta 这个例子中,如果用新类 DemoSlot 直接继承基类 Parent,就会引发报错 metaclass conflict 。


5. Class metaprogramming

最后说一下 metaprogramming 和 class metaprogramming 这两个术语。

  1. metaprogramming 和普通的 programming 相对。普通 programming 编写普通的 class 和 function 等,这些 class 和 function 用于实现直接的功能,比如计算求和,求平均值等功能。

  2. metaprogramming 是指编写一些 meta code,这些 meta code 用于创建或修改其它 class,function 。meta code 本身并不实现直接的功能
    常见的 metaprogramming 包括:装饰器 decorator,工厂函数 factory function,以及 metaclass 等,因为它们都是用于创建或修改其它 function 或 class 。
    (meta code 是我自己编的一个词语。主要目的是把它和普通的 code 区别开,表示它的作用和普通 code 不同)

  3. class metaprogramming 则是指创建的 meta code 专用于对其它 class 进行创建和修改。class decorator ,__init_subclass__ , class factory 和 metaclass 都属于 class metaprogramming 。


—————————— 本文结束 ——————————

  • 8
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值