Python元类(metaclass)的例子

Posted wuhui_gdnt

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python元类(metaclass)的例子相关的知识,希望对你有一定的参考价值。

作者: Eli Bendersky

原文链接:https://eli.thegreenplace.net/2011/08/14/python-metaclasses-by-example

Python理直气壮地为自己是一个相对简单明了的语言而骄傲,在其工作及特性中没有藏着太多“魔术”。不过,有时为了使有趣的抽象成为可能,你可以深入Python尘土飞扬与隐晦的角落,找出比平常更神奇的语言构造。Metaclass是这样一个特性。

不幸的是,metaclass以“寻求问题的解决方案”而著称。本文的目的是展示在广泛使用的Python代码中几个metaclass的实际使用。

网上有许多关于Python metaclass的资料,因此这不只是另一个教程(至于我认为有用的某些链接,查看下面的参考文献部分)。我将花一些时间来解释metaclass是什么,但我的主要目的是这些例子。也就是说,本文仍然渴望成为自包含——即使你不知道metaclass是什么,你也可以开始读它。

在我们开始之前还有一个简短的说明——本文关注在Python 2.6 & 2.7,因为你在网上找到的大部分代码仍然是用于这些版本的【1】。在Python 3.x中,metaclass的工作类似,虽然声明它们的语法有点不同。因此,本文的大部分也适用于3.x

类也是对象

为了理解metaclass,首先我们应该澄清一下类。在Python中,一切都是对象。这包括类。实际上,Python里的类是第一类对象(first-class object)【2】——它们可以在运行时创建,作为参数传递并从函数返回,以及赋值给变量。下面是一个短的交互会话,它展示了类的这些性质:

>>> def make_myklass(**kwattrs):

...   return type('MyKlass', (object,), dict(**kwattrs))

...

>>> myklass_foo_bar = make_myklass(foo=2, bar=4)

>>> myklass_foo_bar

<class __main__.MyKlass>

>>> x = myklass_foo_bar()

>>> x

<__main__.MyKlass object at 0x01F6B050>

>>> x.foo, x.bar

(2, 4)

这里,我们使用了type内置函数的3实参形式来动态创建一个名为MyKlass的类,它继承自object,某些属性作为实参提供。然后我们创建这样一个类。正如你看到的,myklass_foo_bar等同于:

class MyKlass(object):

  foo = 2

  bar = 4

不过它在运行时创建,从一个函数返回并赋值给一个变量。

一个类的类

Python中每个对象(包括内置对象)有一个类。我们已经看到类也是对象,因此类必须也有一个类,对吗?完全正确。Python让我们使用__class__属性检查一个对象的类。让我们来看看这个:

>>> class SomeKlass(object): pass

...

>>> someobject = SomeKlass()

>>> someobject.__class__

<class __main__.SomeKlass>

>>> SomeKlass.__class__

<type 'type'>

我们创建了一个类以及该类的一个对象。检查某个对象的__class__,我们看到它是SomeKlass。接下来是有趣的部分。SomeKlass的类是什么?我们可以使用__class__再次检查它,我们看到它的类型。

TypePython类的类【3】。换而言之,虽然在上面的例子里someobject是一个SomeKlass对象,SomeKlass本身是一个type对象。

我不知道你的情况,但我觉得这让人安心。因为我们认识到在Python里类是对象,它们也有一个类也是合理的,知道有一个作为类的类角色的内置类(type)是令人高兴的。

Metaclass

Metaclass被定义为“一个类的类”。任何实例本身也是类的类是一个metaclass。因此,根据我们上面看到的,这使得type成为一个metaclass——事实上,Python中最常用到的metaclass,因为它是所有类的缺省metaclass

因为一个metaclass是一个类的类,它用于构造类(就像类用于构造对象)。但等一下,我们不是使用一个标准的class定义来创建类吗?当然,但Python在幕后这样做:

  • 在它看到一个class定义时,Python执行它将属性(包括方法)收集到一个字典里。
  • 在该class定义结束时,Python确定这个类的metaclass。让我们称它为Meta
  • 最后,Python执行Meta(name, bases, dct),其中:
    • Meta是这个metaclass,因此这个定义在具现(instantiating)它。
    • Name是这个新创建类的名字
    • Bases是这个类基类的元组
    • Dct将属性名映射到对象,列出这个类的所有属性

我们如何确定一个类的metaclass呢?简单来说【4】,如果一个类或其中一个基类有一个__metaclass__属性【5】,该属性作为metaclass。否则,type是这个metaclass

那么我们这样定义时发生了什么:

class MyKlass(object):

  foo = 2

是这样:MyKlass没有__metaclass__属性,因此使用type,类的创建这样完成:

MyKlass = type(name, bases, dct)

这与我们在文章开头看到的一致。另一方面,如果MyKlass定义了一个metaclass

class MyKlass(object):

  __metaclass__ = MyMeta

  foo = 2

那么这个类的创建这样完成:

MyKlass = MyMeta(name, bases, dct)

这样,应该恰当地实现MyMeta,以支持这样的调用形式并返回这个新的类。它实际上类似于使用一个预定义的构造函数签名,编写一个普通类。

Metaclass的__new__与__init__

metaclass里为了控制类的创建与初始化,你可以实现metaclass__new__方法及/__init__构造函数【6】。大多数现实生活中的metaclass可能只重载其中一个。在你希望控制一个新对象的创建(我们的情形里类)时,应该实现__new__;而在你希望在这个新对象创建后,控制它的初始化时,应该实现__init__

因此在上面的MyMeta调用完成时,幕后发生了这个:

MyKlass = MyMeta.__new__(MyMeta, name, bases, dct)

MyMeta.__init__(MyKlass, name, bases, dct)

下面是一个更具体的例子,它展示发生了什么。让我们为一个metaclass写下这样的定义:

class MyMeta(type):

    def __new__(meta, name, bases, dct):

        print '-----------------------------------'

        print "Allocating memory for class", name

        print meta

        print bases

        print dct

        return super(MyMeta, meta).__new__(meta, name, bases, dct)

    def __init__(cls, name, bases, dct):

        print '-----------------------------------'

        print "Initializing class", name

        print cls

        print bases

        print dct

        super(MyMeta, cls).__init__(name, bases, dct)

Python执行下面的类定义时:

class MyKlass(object):

    __metaclass__ = MyMeta

 

    def foo(self, param):

        pass

 

    barattr = 2

打印出这样(为了清晰重新格式化了):

-----------------------------------

Allocating memory for class MyKlass

<class '__main__.MyMeta'>

(<type 'object'>,)

'barattr': 2, '__module__': '__main__',

 'foo': <function foo at 0x00B502F0>,

 '__metaclass__': <class '__main__.MyMeta'>

-----------------------------------

Initializing class MyKlass

<class '__main__.MyKlass'>

(<type 'object'>,)

'barattr': 2, '__module__': '__main__',

 'foo': <function foo at 0x00B502F0>,

 '__metaclass__': <class '__main__.MyMeta'>

研究、理解这个例子,你将掌握编写元类所需的大部分知识。

需要注意的是,这些打印输出实际上是在类创建时完成的,即包含这个类的模块第一次被导入时。记住这个细节,以后再说。

Metaclass的__call__

另一个偶尔重载有用的metaclass方法是__call__。我把它与__new____init__分开讨论的原因是,不像这两个方法在类创建时刻被调用,在调用已经创建的类具现一个新对象时调用__call__。下面的代码澄清了这一点:

class MyMeta(type):

    def __call__(cls, *args, **kwds):

        print '__call__ of ', str(cls)

        print '__call__ *args=', str(args)

        return type.__call__(cls, *args, **kwds)

 

class MyKlass(object):

    __metaclass__ = MyMeta

 

    def __init__(self, a, b):

        print 'MyKlass object with a=%s, b=%s' % (a, b)

 

print 'gonna create foo now...'

foo = MyKlass(1, 2)

这打印出:

gonna create foo now...

__call__ of  <class '__main__.MyKlass'>

__call__ *args= (1, 2)

MyKlass object with a=1, b=2

这里MyMeta.__call__只是告诉我们实参,并把它们委托给type.__call__。不过,它也可以干涉这个过程,影响类对象创建的方式。在某种程度上,这并非不像重载类本身的__new__方法,尽管存在某些差异【7】。

例子

我们现在已经涉及了足够多的理论,理解metaclass是什么,以及如何编写它们。现在是时候使用例子来进一步澄清了。如我在上面提到的,我更希望研究真正Python代码里metaclass的使用,而不是编写人造的例子。

string.Template

metaclass的第一个例子取自Python标准库。它是Python自带的非常少数几个metaclass之一。

String.Template提供了便利的、具名的字符串替换,可以作为一个非常简单的模板系统。如果你不熟悉这个类,这将是阅读文档的好时机。我将只是解释它如何使用metaclass。下面是class Template的前几行代码:

class Template:

    """A string class for supporting $-substitutions."""

    __metaclass__ = _TemplateMetaclass

 

    delimiter = '$'

    idpattern = r'[_a-z][_a-z0-9]*'

 

    def __init__(self, template):

        self.template = template

这是_TemplateMetaclass

class _TemplateMetaclass(type):

    pattern = r"""

    %(delim)s(?:

      (?P<escaped>%(delim)s) |   # Escape sequence of two delimiters

      (?P<named>%(id)s)      |   # delimiter and a Python identifier

      (?P<braced>%(id)s)   |   # delimiter and a braced identifier

      (?P<invalid>)              # Other ill-formed delimiter exprs

    )

    """

 

    def __init__(cls, name, bases, dct):

        super(_TemplateMetaclass, cls).__init__(name, bases, dct)

        if 'pattern' in dct:

            pattern = cls.pattern

        else:

            pattern = _TemplateMetaclass.pattern %

                'delim' : _re.escape(cls.delimiter),

                'id'    : cls.idpattern,

               

        cls.pattern = _re.compile(pattern, _re.IGNORECASE | _re.VERBOSE)

本文第一部分里的解释对理解_TemplateMetaclass如何工作应该是足够的。其__init__方法检查某些类属性(特别的,patterndelimiteridpattern),并使用它们来构建一个编译后的regex,然后这被保存回这个类的pattern属性。

根据文档,可以继承Template来提供一个定制的分隔符与ID模式,或者整个regex。在类构建时刻,这个metaclass确保这些被转换为一个编译后的regex模式,因此这是一种优化。

我的意思是,通过在构造函数里构建编译后的regex,无需使用metaclass,可以实现相同的定制。不过,这意味着该编译步骤在每次具现一个Template对象时完成。考虑下面的使用,恕我直言与string.Template是一样:

>>> from string import Template

>>> Template("$name is $value").substitute(name='me', value='2')

'me is 2'

regex编译留待Template具现时刻,意味着每次运行这样的代码片段时,它将被创建及编译。这是让人难为情的——因为regex不是真正依赖这个模板字符串,而是仅依赖该类的属性。

使用metaclass,在载入该模块以及将执行类Template(或其子类)定义时,pattern类属性仅创建一次。在创建Template对象时,这节省了时间且合理,因为在类创建时刻,我们有编译regex所需的所有信息——因此为什么要推迟这个操作呢?

有人可能会声称这是一个不成熟的优化,这是真的。我不准备为metaclass的这个(或任何)使用辩护。这里我的目的只是展示,在真实代码里,metaclass如何用于各种任务。因此,对于这个教学目的,它是一个良好的例子,因为它展示了一个有趣的用例。不管是否是不成熟的优化,通过在代码执行的过程里将计算提前一步,metaclass确实使得代码更高效。

twisted.python.reflect.AccessorType

下面的例子是metaclass一个频繁使用的展示。其文档的一个摘录:

自动生成属性的metaclass。对你的类使用这个metaclass将给予你显式的访问方法;一个称为set_foo的方法将自动创建把set_foo用作一个设定方法的属性fooGet_foodel_foo也一样。

下面是这个metaclass,缩短了一点以强调重要的部分:

class AccessorType(type):

    def __init__(self, name, bases, d):

        type.__init__(self, name, bases, d)

        accessors =

        prefixs = ["get_", "set_", "del_"]

        for k in d.keys():

            v = getattr(self, k)

            for i in range(3):

                if k.startswith(prefixs[i]):

                    accessors.setdefault(k[4:], [None, None, None])[i] = v

        for name, (getter, setter, deler) in accessors.items():

            # create default behaviours for the property - if we leave

            # the getter as None we won't be able to getattr, etc..

 

            # [...] some code that implements the above comment

 

            setattr(self, name, property(getter, setter, deler, ""))

这做得很简单:

  1. 找出所有以get_set_del_开头的类属性
  2. 通过它们意在控制的属性组织它们(它们名字中下划线后的部分)
  3. 对每个找到的三元组gettersetterdeler
    1. 确保这三个都存在,或者创建合适的缺省
    2. 将它们设置为这个类的一个属性

这样一个metaclass用处多大?很难说。Twisted本身不使用它,但把它提供为一个公共的API。如果你有几个类有许多属性要编写,这个metaclass可能节省相当多的代码。

pygments Lexer与RegexLexer

pygments库展示了metaclass使用的一个有趣的风格(idiom)。一个基类使用一个定制metaclass来创建。用户类可以从这个基类继承,获得这个metaclass作为奖赏【8】。首先,让我们看一下LexerMeta metaclass,它用作Lexermetaclass——pygments里词法分析器的基类:

class LexerMeta(type):

    """

    This metaclass automagically converts ``analyse_text`` methods into

    static methods which always return float values.

    """

 

    def __new__(cls, name, bases, d):

        if 'analyse_text' in d:

            d['analyse_text'] = make_analysator(d['analyse_text'])

        return type.__new__(cls, name, bases, d)

这个metaclass重载了__new__方法来拦截analyse_text消息的定义,并它转换为总是返回一个浮点值的静态方法(这是make_analysator函数做的)。

注意这里使用__new__而不是__init__。为什么不使用__init__?在我看来,这只是喜好问题——重载__init__也能实现相同的效果。

Pygments的第二个例子更复杂,但值得花力气去解释,因为它包含了我们在之前例子里没有看到的几个特性。RegexLexerMeta的代码相当长,因此我只留下相关的部分:

class RegexLexerMeta(LexerMeta):

    """

    Metaclass for RegexLexer, creates the self._tokens attribute from

    self.tokens on the first instantiation.

    """

 

    # [...] snip

 

    def __call__(cls, *args, **kwds):

        """Instantiate cls after preprocessing its token definitions."""

        if not hasattr(cls, '_tokens'):

            cls._all_tokens =

            cls._tmpname = 0

            if hasattr(cls, 'token_variants') and cls.token_variants:

                # don't process yet

                pass

            else:

                cls._tokens = cls.process_tokendef('', cls.tokens)

 

        return type.__call__(cls, *args, **kwds)

一般而言,代码相当清晰——metaclass检查tokens类属性,并从它创建_tokens。这仅在该类的第一次具现时完成。这里有两件特别有趣的事:

  1. RegexLexerMetaLexerMeta继承,因此其使用者也得到了LexerMeta提供的服务。Metaclass的继承是它们成为Python中最强大语言构造的原因之一。例如,将这与类修饰符(decorator)相比。对于某些简单的任务,类修饰符可以替代metaclass,但metaclass形成继承关系的能力是修饰符做不到的。
  2. Process_tokendef计算在__call__中执行——以及一个特殊的检查确保它实际上仅在该类的第一次具现时运行(虽然__call__本身对所有的具现调用)。为什么它像这样,而不是在类创建时刻(例如在metaclass__init__)?在我看来这可能是一种优化。Pygments带有许多词法分析器【9】,但在任何代码里,你可能只希望使用一或两个。为什么花费时间载入你不需要的词法分析器,而不是只载入你需要的呢?不管这是否是真正的原因,我认为它仍然是metaclass值得深思的,有趣一方面——它们提供给你选择何处及如何执行它们元工作(meta-work)的极大的灵活性。

结论

在本文中我的目的是解释Python里的metaclass如何工作,并提供真实Python代码里metaclass使用的几个具体例子。我知道metaclass有一些恶名,因为许多人认为它们故弄玄虚。对这个问题我的看法是,像其他语言构造,metaclass是一个工具,程序员最终负责正确地使用它。总是编写完成任务的最简单的代码,但如果你觉得metaclass是你需要,你可以自由使用metaclass

我希望本文展示了metaclass对定制类创建与使用的方式提供的极大的灵活性。这些例子展现了metaclass实现与使用的几个方面——重载__init____new____call__方法,使用metaclass继承,向类添加属性,将对象方法转换为静态方法以及在类定义或具现时刻执行优化。

Python里,metaclass最值得注意的例子可能是它们在ORM(对象关系映射,Object Relational Mapping)框架中的使用,比如Django的模型。实际上,这些都是metaclass能力的有力展现,但我决定不在这里展示它们,因为它们的代码是复杂的,并且许多领域特定的细节模糊了展示metaclass的主要目的。不过,读了本文后,你就有了理解更复杂例子所需的一切。

P.S. 如果发现了metaclass其他有趣的例子,请告诉我。对另外的真实使用,我很有兴趣。

参考文献

[1]

此外,我仅触及新形式类(即从object派生的类)。我希望你编写的所有新代码以及这些天你看到的大部分代码使用新形式类。使用旧式类,这里展示的metaclass某些细节有点不同,虽然总的来说适用相同的原则。

 

[2]

这里的文字游戏是无意的。在“第一类对象”中,词“类”有与“等级(rank)”或“量器(caliber)”相同的含义,暗示着在该语言里这些对象不会比其他对象更差。

 

[3]

这里你可能注意到了表里不一——type同时是一个类以及用于创建新类及检测对象类型的内置类。这与Python中的其他内置没有不同。例如,dict同时是字典(.__class__is <type ‘dict’>) 的类,以及字典的一个构造函数。事实上,对用户定义类这也成立,因为这个类的名字也作为新对象的构造函数。因此你可以将type视为Python里的另一个内置类。

 

[4]

你可以在data model reference里查找实际规则。

 

以上是关于Python元类(metaclass)的例子的主要内容,如果未能解决你的问题,请参考以下文章

Python元类(metaclass)的例子

深刻理解Python中的元类(metaclass)

深刻理解Python中的元类(metaclass)

深刻理解Python中的元类(metaclass)

深刻理解Python中的元类(metaclass)

深刻理解Python中的元类(metaclass)

(c)2006-2019 SYSTEM All Rights Reserved IT常识

[5]

再次的,提醒在本文里我描述Python 2。在Python 3里,调用metaclass的语法已经变了,更直观一些。