Understanding Python metaclasses

原文地址:https://blog.ionelmc.ro/2015/02/09/understanding-python-metaclasses/

译文参考:https://blog.csdn.net/wwx890208/article/details/80644400

国外的一位程序员(Ionel)在网上看了很多讲述Python元类的文章,但觉得没有一篇能讲明白元类在Python中是如何工作的,所以 Ionel 写了这篇文章,给出了自己的理解。

Python中,元类是一个充满争议的话题,很多开发者都避免使用元类。我认为导致这种现象的主要原因是,Python学习过程中没有很好地理解清楚Python灵活的工作流和查找规则。您需要理解一些关键的概念,才能在Python中有效地使用元类。

如果您以前没听过元类,那么简单来说,元类提供了很多有趣的途径,来减少模版的使用,并提供更加友好的API。为了使本文尽可能精炼,我假设读者们都富有创造性,这样我就不需要花大量的篇幅去解释:“为什么A需要用元类来实现,而B不需要?”。

本文使用Python 3,如果有Python 2值得注意的地方,会在脚注中用小字说明(脚注没翻译,看原文吧)。

概述

在深入讨论细节之前,我们有必要先作一个概括性的说明。

在Python中,类也是对象。与普通对象一样,类也是某个东西的实例——那就是元类。默认的元类是 type。不幸的是,为了能向后兼容, type有点难以理解: type可以作为一个函数使用,返回对象所属的类。

>>> class Foobar:
...     pass
...
>>> type(Foobar)
<class 'type'>
>>> foo = Foobar()
>>> type(foo)
<class '__main__.Foobar'>

如果熟悉内置函数 isinstance,您应该知道:

>>> isinstance(foo, Foobar)
True
>>> isinstance(Foobar, type)
True

上述关系如图所示:

在这里插入图片描述

让我们继续回到类的创建上来……

元类的简单使用

创建类时,我们可以完全不用 class声明语句,而是用 type来直接创建:

>>> MyClass = type('MyClass', (), {})
>>> MyClass
<class '__main__.MyClass'>

class声明不仅仅是语法糖,它还做了很多额外的工作,比如设置恰当的 __qualname____doc__属性,以及调用 __prepare__方法等。

我们可以自定义一个元类:

>>> class Meta(type):
...     pass

然后使用它:

>>> class Complex(metaclass=Meta):
...     pass
>>> type(Complex)
<class '__main__.Meta'>

现在,我们对元类的使用有了大概的了解……

魔法方法

魔法方法是Python的特性之一,它允许开发人员重载各种操作符以及对象的行为。您可以这样做,来重载调用操作符:

>>> class Funky:
...     def __call__(self):
...         print("Look at me, I work like a function!")
>>> f = Funky()
>>> f()
Look at me, I work like a function!

元类的实现依赖于魔法方法,因此我们需要对这些魔法方法有所了解。

slots

当您在类中定义一个魔法方法时,此方法最终会成为描述该类的结构体中的一个指针,并放入 __dict__。对每个魔法方法,这个结构体中都有对应的字段。出于一些原因,这些字段被称为 type slots

而Python通过 __slots__属性实现了另外一个特性。带有 __slots__属性的类,创建的实例是没有 __dict__属性的(这样实例占的内存会少一丁点)。随之而来的副作用也很明显,除了 __slots__中指定的属性,实例不能再拥有其他属性:如果尝试给实例设置一个不在 __slots__中的属性,Python会抛出异常。

需要注意是,本文提到的 slots ,指的都是 type slots ,而不是 __slots__

对象属性查找

这里很容易出错,因为和Python 2的经典类相比,Python 3新式类的实现有了很多细微的差别。

这里假设 Class是一个类, instance是类 Class的一个实例(为了和下一节区别,这里可理解为: instance是一个实例对象),那么引用 instance.foobar属性时的查找流程大致如下:

  • 调用 Class.__getattribute__tp_getattro)对应的 type slot 。此方法的默认实现如下:

    • Class.__dict__中是否存在名为 foobar的元素,此元素有 __get__方法,且是数据描述符?
      • 若存在,返回 Class.__dict__['foobar'].__get__(instance, Class)的返回值。
    • instance.__dict__中是否存在名为 foobar的元素?
      • 若存在,返回 instance.__dict__['foobar']
    • Class.__dict__中是否存在名为 foobar的元素,此元素有 __get__方法,但不是数据描述符?
      • 若存在,返回 Class.__dict__['foobar'].__get__(instance, Class)的返回值。
    • Class.__dict__中是否存在名为 foobar的元素?
      • 若存在,则返回 Class.__dict__['foobar']
  • 若仍未找到 foobar,但存在 Class.__getattr__,则调用 Class.__getattr__('foobar')并返回。

如果还不清楚,可参考下图,下图展示了对象属性查找的整个过程:

在这里插入图片描述

为了避免点号 “.” 造成混淆,图里使用冒号 “:” 表示所处位置。

类属性查找

由于类需要支持类方法和静态方法,所以和引用对象属性( instance.foobar)的查找流程相比,引用类属性( Class.foobar)时的查找流程略有不同。

假设 ClassMetaclass的一个实例(为了和上一节区别,这里可理解为: Class是一个类对象),引用 Class.foobar时的流程大致如下:

  • 调用 Metaclass.__getattribute__tp_getattro)对应的 type slot 。此方法的默认实现如下:

    • Metaclass.__dict__中是否存在名为 foobar的元素,此元素有 __get__方法,且是数据描述符?
      • 若存在,返回 Metaclass.__dict__['foobar'].__get__(Class, Metaclass)的返回值。
    • Class.__dict__中是否存在名为 foobar的元素,且此元素是描述符(数据/非数据描述符)?
      • 若存在,返回 Class.__dict__['foobar'].__get__(None, Class)的返回值。
    • Class.__dict__中是否存在名为foobar的元素?
      • 若存在,返回 Class.__dict__['foobar']
    • Metaclass.__dict__中是否存在名为 foobar的元素,但此元素不是数据描述符?
      • 若存在,返回 Metaclass.__dict__['foobar'].__get__(Class, Metaclass)的返回值。
    • Metaclass.__dict__中是否存在名为foobar的元素?
      • 若存在,则返回 Metaclass.__dict__['foobar']
  • 若仍未找到 foobar属性,但存在 Metaclass.__getattr__,则调用 Metaclass.__getattr__('foobar')并返回。

整个流程如下图所示:

在这里插入图片描述

为了避免点号 “.” 造成混淆,图里使用冒号 “:” 表示所处位置。

魔法方法查找

魔方方法的查找是在类上完成的,直接使用类的大结构体的 slots。

  • 对象的类中是否存在此魔法方法对应的 slot (C代码大致如下: object->ob_type->tp_<magicmethod>)?

    • 若存在,则直接使用。
    • 否则,对应的指针为 NULL,表示不支持此魔法方法对应的操作。

    内部C代码的含义:

    • object->ob_type表示 object 对应的类。
    • ob_type->tp_<magicmethod>表示 type slot

上述流程看似简单,实际上 type slots 指向的是包装器,包装器包装您自定义的函数,因此描述符才能按照预期工作:

>>> class Magic:
...     @property
...     def __repr__(self):
...         def inner():
...             return "It works!"
...         return inner
...
>>> repr(Magic())
'It works!'

这就是魔方方法的实现。那么,有没有不遵守上述流程,以不同的方式来查找 slot 的呢?很遗憾,答案是 Yes ,让我们继续……

__new__方法

__new__方法是类和元类之间最容易混淆的方法之一。__new__方法有些非常特殊的约定。

__new__方法是一种构造器(它返回新的实例),__init__方法只是一个初始化程序(当 __init__被调用时,实例已经创建好了)。

假设有个类,定义如下:

class Foobar:
    def __new__(cls):
        return super().__new__(cls)

如果您学习过上一节,会觉得应该在元类上查找 __new__方法。不过遗憾的是,如果这样查找的话,__new__的作用将大大减弱,而实际上,__new__方法是静态查找的。

Foobar类需要使用这个魔法方法时,它会在当前的对象(即 Foobar类)上查找,而不像其他魔法方法,在其上一层查找。理解这一点非常重要,因为类和元类都可以定义 __new__方法。

  • Foobar.__new__被用来创建 Foobar的实例
  • type.__new__被用来创建 Foobar类(type的一个实例)

__prepare__方法

__prepare__方法在 class主体执行前调用,且必须返回一个类字典(dict-like)的对象,此对象被当作一个局部命名空间,这个命名空间包含了 class主体的所有代码。此方法是Python 3.0新增的功能,详见PEP-3115

如果您自定义的 __prepare__返回一个对象 x,那么下面这段代码:

class Class(metaclass=Meta):
    a = 1
    b = 2
    c = 3

会这样修改 x

x['a'] = 1
x['b'] = 2
x['c'] = 3

这个 x对象需要看起来像一个字典。需要注意的是,这个 x对象最终会成为 Metaclass.__new__方法的一个参数,并且如果它不是 dict的实例,您还需要在调用 super().__new__前手动转换它。

有趣的是,__prepare____new__不同,并没有特殊的查找方式。似乎它没有属于自己的 type slot ,但如果您阅读过前面的章节,就会知道,__prepare__的查找是通过类属性查找方式来实现的。

把它们放在一起

首先,如图所示,一个实例是如何被构建的:

在这里插入图片描述

如何理解这个泳道图呢?

  • 水平区域是自定义的函数。
  • 实线表示函数调用。
    • Metaclass.__call__Class.__new__的实线,表示 Metaclass.__call__将要调用 Class.__new__
  • 虚线表示返回值。
    • Class.__new__返回 Class的实例。
    • Metaclass.__call__返回 Class.__new__的返回值(如果 Class.__new__返回的是 Class的一个实例,Metaclass.__call__还会在这个实例上调用 Class.__init__)。
  • 红圆圈中的数字记录了调用顺序。

创建一个类的流程很类似:

在这里插入图片描述

有一些需要注意的地方:

  • Metaclass.__prepare__只返回命名空间对象(如前文所述,是一个类字典 dict-like 对象)。
  • Metaclass.__new__返回 Class对象。
  • MetaMetaclass.__call__返回 Metaclass.__new__的返回值(如果 Metaclass.__new__返回的是 Metaclass的一个实例, MetaMetaclass.__call__还会在这个实例上调用 Metaclass.__init__)。

可见,元类允许您自定义一个对象生命周期的几乎每个部分。

元类是可调用的

如果再仔细看一遍前两张图,您会注意到,创建实例是通过调用 Metaclass.__call__实现的。这意味着,我们可以用任何可调用的东西作为元类。

>>> class Foo(metaclass=print):  # pointless, but illustrative
...     pass
...
Foo () {'__module__': '__main__', '__qualname__': 'Foo'}
>>> print(Foo)
None

如果您使用函数作为元类,那么子类将不能继承这个“函数元类”,而继承这个函数返回的类型。

子类继承元类

和类装饰器相比,元类的优势之一是:元类是可以被子类继承的。

这是因为 Metaclass(...)返回了一个对象,这个对象的 __class__Metaclass

多个元类的限制

Python允许一个类有多个父类,每个父类又可以有不同的元类。但幸运的是,一切都必须是线性的——即继承树只能有一片叶子。

例如下述代码,是非法的Python程序,因为存在2片叶子( Meta1Meta2):

>>> class Meta1(type):
...     pass
...
>>> class Meta2(type):
...     pass
...
>>> class Base1(metaclass=Meta1):
...     pass
...
>>> class Base2(metaclass=Meta2):
...     pass
...
>>> class Foobar(Base1, Base2):
...     pass
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

这样才能正常工作(并且使用叶子做为元类):

>>> class Meta(type):
...     pass
...
>>> class SubMeta(Meta):
...     pass
...
>>> class Base1(metaclass=Meta):
...     pass
...
>>> class Base2(metaclass=SubMeta):
...     pass
...
>>> class Foobar(Base1, Base2):
...     pass
...
>>> type(Foobar)
<class '__main__.SubMeta'>

方法签名

本文还缺少一些重要的细节,比如方法的签名。让我们通过类和元类来看看这些重要的实现。

注意 **kwargs—— 用来收集其他不需要关注的关键字参数,这样就可以直接将它们传递给 class声明语句。

>>> class Meta(type):
...     @classmethod
...     def __prepare__(mcs, name, bases, **kwargs):
...         print('  Meta.__prepare__(mcs=%s, name=%r, bases=%s, **%s)' % (
...             mcs, name, bases, kwargs
...         ))
...         return {}

之前提到过,__prepare__方法返回的对象可能并不是 dict的实例。因此,您必须确保在自定义的 __new__方法中处理这种情况。

...     def __new__(mcs, name, bases, attrs, **kwargs):
...         print('  Meta.__new__(mcs=%s, name=%r, bases=%s, attrs=[%s], **%s)' % (
...             mcs, name, bases, ', '.join(attrs), kwargs
...         ))
...         return super().__new__(mcs, name, bases, attrs)

在元类中自定义 __init__并不常见,因为 __init__的作用并不大——当 __init__被调用时,类已经构建好了。元类的 __init__作用大致相当于一个类装饰器,二者的区别在于,创建子类时,元类的 __init__会运行,而子类并不会调用父类的类装饰器。

...     def __init__(cls, name, bases, attrs, **kwargs):
...         print('  Meta.__init__(cls=%s, name=%r, bases=%s, attrs=[%s], **%s)' % (
...             cls, name, bases, ', '.join(attrs), kwargs
...         ))
...         return super().__init__(name, bases, attrs)

当您创建 Class的实例时,__call__方法会被调用。

...     def __call__(cls, *args, **kwargs):
...         print('  Meta.__call__(cls=%s, args=%s, kwargs=%s)' % (
...             cls, args, kwargs
...         ))
...         return super().__call__(*args, **kwargs)
...

使用 Meta(注意 extra=1):

>>> class Class(metaclass=Meta, extra=1):
...     def __new__(cls, myarg):
...         print('  Class.__new__(cls=%s, myarg=%s)' % (
...             cls, myarg
...         ))
...         return super().__new__(cls)
...
...     def __init__(self, myarg):
...         print('  Class.__init__(self=%s, myarg=%s)' % (
...             self, myarg
...         ))
...         self.myarg = myarg
...         return super().__init__()
...
...     def __str__(self):
...         return "<instance of Class; myargs=%s>" % (
...             getattr(self, 'myarg', 'MISSING'),
...         )
  Meta.__prepare__(mcs=<class '__main__.Meta'>, name='Class', bases=(),
                   **{'extra': 1})
  Meta.__new__(mcs=<class '__main__.Meta'>, name='Class', bases=(),
               attrs=[__qualname__, __new__, __init__, __str__, __module__],
               **{'extra': 1})
  Meta.__init__(cls=<class '__main__.Class'>, name='Class', bases=(),
                attrs=[__qualname__, __new__, __init__, __str__, __module__],
                **{'extra': 1})

注意,当我们创建 Class的实例时,Meta.__call__会被调用:

>>> Class(1)
  Meta.__call__(cls=<class '__main__.Class'>, args=(1,), kwargs={})
  Class.__new__(cls=<class '__main__.Class'>, myarg=1)
  Class.__init__(self=<instance of Class; myargs=MISSING>, myarg=1)
<instance of Class; myargs=1>

尾声

现在,我已经把一切都写了出来,似乎写了很多,比我想象的还要多。但我觉得还是遗漏了什么重要的细节 😃

下一篇文章,我们将讨论这些元类理论的实际应用……

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值