[Python 进阶] Python元类(Metaclass)入门和简单应用

元类,就是"类的类"

由Jeza Chen 发表于 January 9, 2020

最近在阅读《Python Cookbook》,看到第九章的时候有些关于元编程的地方还是不太了解,特别是有关元类的那一部分,所以找了一下网上一些教程文档,弄通了元类后,自己再总结一下有关元类的一些基本知识以及常见的用法。

其实,元类并不难理解,只要我们走出类的思维框架,以更高的层次去看待类本身是如何构造出来的,就弄懂了元类的概念。进而使用元类去“干涉”类。

1. 从type说起

我们都清楚,创建一个实例(Instance)的方式是通过调用类(Class)的构造函数(Constructor):

>>> class A:
...     def __init__(self, name):
...             self.name = name
...     def spam(self):
...             print("name:", self.name)
... 
>>> a = A("jezachen")
>>> a.spam()
name: jezachen

我们也知道,type()是一个可以查看一个变量或者类型的类型,从以下解释器的运行结果可以知道,a是一个实例,它的类型是A;而A是一个类,它的类型是type:

>>> type(a)
<class '__main__.A'>
>>> type(type(a))
<class 'type'>

可以发现,type本身是一个class,而class A的类型却是type,那么是否可以说明类A实质上是type的一个实例呢?

答案是正确的,type本身是一个class,所谓的type()函数,实质上也具有构造函数的功能,可以直接通过调用type()函数去动态生成一个等价的A类:

>>> def __init__(self, name):
...     self.name = name
... 
>>> def spam(self):
...     print("name:", self.name)
... 
>>> A = type("A", (object,), {"__init__":__init__, "spam":spam})
>>> a = A("jeza chen")
>>> a.spam()
name: jeza chen

Python作为一个动态语言,强大的特性不得不叹为观止。我们甚至可以通过一个type()函数动态生成一个类,这也意味着在Python中似乎有一个比类(Class)更加抽象、高级的层次,似乎如上帝一般创建世间万物。这就是Python元编程一个重要特性——元类(Metaclass)。

在说明元类前,我们还是要弄清楚type()函数(亦type类的构造函数)的参数意义,以弄懂后面元类的__new__函数打下基础:

type(name, bases, dict)

其中,

name:字符串类型,指定类的名字;

bases:元组(tuple)类型,指定所继承的类。在上文,这个参数是(object,)(注意,逗号不能省略,否则不是元组),意味着类A继承于object,也就是类A就是所谓的新式类;

dict:字典(dict)类型,指定该类的体(body)所包含的所有属性(attributes)和方法(method)。在上文,该参数是{"__init__":__init__, "spam":spam},而作为键值的__init__spam函数则是在调用type()函数前定义完毕。

因此,再强调一遍,下面两个定义类A的方式是等价的:

>>> class A:
...     def __init__(self, name):
...             self.name = name
...     def spam(self):
...             print("name:", self.name)
... 
A = type("A", (object,), {"__init__":__init__, "spam":spam})

事实上,也不难推理得知,Python解释器在遇到class ... 关键字时会一步步解析类的内容,最终调用 type(...) (准确说是指定的元类)的构造函数来创建类。而我们后面所学的元类知识,其实是掌握如何去”干涉“元类的构造函数(或__new__函数),从而”批量制造“出我们想要的类

弄懂上面这三个参数的意义后,我们就比较容易过渡到元类的学习了。

2. 初窥元类(Metaclass)

通过上面我们可以知道,一个类可以使用type()函数(即type类的构造函数)来动态创建。那么,我们是否能自己定义一个继承于type的类,从而控制类的创建过程呢?答案当然是可以的。

先说实例(Instance)与类(Class)的关系,一个实例,可以通过类来创建出实例。即先定义类,再创建实例;

那么,同理,一个类(Class),可以通过元类(Metaclass)来创建出类。即先定义元类,在创建类。

换句话说,类,就是元类的一个实例。

而元类,要么是type本身,要么是继承于type的子类。

元类可以说是Python中最难理解和运用的一部分,但其丰富的特性使Python编程带来了更多的可能性与创造性。在正常情况下,普通的程序员不会碰到太多的元编程;但是,如果你对一些大型的框架感兴趣的话,元类是不得不接触到的重要内容。

在详细讲述元类之前,我们可以通过以下代码,感受元类的魅力:

在某种情况下,我们可能需要在某些类上实现单例模式(Singleton)。在一般情况下,我们可能使用这种方式来实现:

class Spam:
    def __new__(cls, *args, **kwargs):
        if not hasattr(Spam, "_instance"):
            setattr(Spam, "_instance", object.__new__(cls))
        return getattr(Spam, "_instance")

    def __init__(self):
        print("Creating Spam")

运行解释器,可以得到如下结果:

>>> a = Spam()
Creating Spam
>>> b = Spam()
Creating Spam
>>> a is b
True

我们可以通过__new__()方法成功实现单例模式,但”Creating Spam”的信息却输出了两次。虽然在这里不影响使用,但在一些场合中,极有可能出现多次初始化(customize)的情况,这是我们所不想看到的。当然,我们也可以在__init__()方法中增加一些判定的逻辑以消除这种情况,亦或是通过一个类方法(classmethod)绕过__init__()方法;而且,如果有多个类需要实现单例模式,则要重复多次这些步骤,增加不必要的工作量。

因此,我们可以使用元类去更好地实现单例模式:

class Singleton(type):
    def __init__(self, *args, **kwargs):
        self._instance = None
        super().__init__(*args, **kwargs)

    def __call__(self, *args, **kwargs):
        if self._instance is None:
            self._instance = super().__call__(*args, **kwargs)
            return self._instance
        else:
            return self._instance


class Spam(metaclass=Singleton):
    def __init__(self):
        print("Creating Spam")

运行解释器,看看结果:

 
>>> a = Spam()
Creating Spam
>>> 
>>> b = Spam()
>>> a is b
True

从这里我们可以看到,使用元类不仅可以实现单例模式,且初始化方法只调用了一次!与此同时,如果有多个类需要实现单例模式,只要在该类的定义中指定metaclass为Singleton,在__init__()方法写好对应的初始化逻辑即可,而无需过多的繁琐步骤。这样何乐而不为呢?

3. 元类是如何定义的

在上面我们已经知道,类是元类的一个实例。只要我们定义好元类,就可以按照特定的方式去“批量”生成我们想要的类(如具有单例模式的类)。那么,我们该如何定义一个元类呢?

首先我们要清楚的是,元类需要继承于type类,即:

class MetaclassTest(type):
    ...

这样,一个元类就诞生出来了,但这个和type类是无异的。我们还需要重载一些方法,去实现更多的功能:

3.1 __new__()方法

同理于类创建实例,元类先调用__new__()方法创建一个类(即元类的实例),在__init__()方法对创建出来的类进行初始化(或定制)操作。在该节我们先解释__new__方法所接收到的各个参数的定义:

__new__(cls, clsname, bases, clsdict)

其中:

cls: 元类 clsname: 类的名字 bases: 继承的父类的集合 clsdict: 字典对象,类的属性/方法集合

为方便说明,我们可以定义一个元类,用来打印每个参数的内容:

class PrintParamMeta(type):
    def __new__(cls, clsname, bases, clsdict):
        print("PrintParamMeta.__new__()")
        print(cls, clsname, bases, clsdict, end="\n")
        return super().__new__(cls, clsname, bases, clsdict)


class BaseOfA:
    pass


class A(BaseOfA, metaclass=PrintParamMeta):
    def spam(self):
        print(self.name)

    def __init__(self, name, age):
        print("A.__init__()")
        self.name = name
        self.age = age

    age = 31

    def print_age(self):
        print(self.age)


if __name__ == '__main__':
    print("execute a = A()")
    a = A("a", 22)
    a.spam()
    a.print_age()
    print("execute aa = A()")
    aa = A("aa", 25)
    aa.spam()
    aa.print_age()
    a.print_age()

输出结果:

PrintParamMeta.__new__()
<class '__main__.PrintParamMeta'>
A
(<class '__main__.BaseOfA'>,)
{'__qualname__': 'A', '__module__': '__main__', 'age': 31, 'print_age': <function A.print_age at 0x7f95790f5b70>, 'spam': <function A.spam at 0x7f95790f5a60>, '__init__': <function A.__init__ at 0x7f95790f5ae8>}
execute a = A()
A.__init__()
a
22
execute aa = A()
A.__init__()
aa
25
22

可以看到,元类PrintParamMeta的__new__()函数只调用了一次,即发生在类A的创建期间(注意并不是A的实例a或aa的创建期间);而实例a或实例aa的创建并不会调用类A的__new__()函数(因为此时类A已经创建出来了)。

同时,通过元类PrintParamMeta的__new__()函数所打印的信息可以看到,cls参数为元类PrintParamMeta;而clsname参数为一个字符串,是类A的名字;bases参数为一个元组(tuple),里面的元素是类A的父类;clsdict则是一个字典,里面存储着类A的属性/方法。

因此,我们可以通过定制元类的__new__()方法,无需改动类的定义,去给类实现额外的功能。

3.2 __init__()方法

在上面我们知道,Python先通过元类的__new__()方法创造出一个类,在调用元类的__init__()方法去初始化这个类。这和类创造实例的过程是差不多的。__init__()方法的各个参数和__new__()方法是差不多的,除了第一个参数从cls变成了self(因为此时类对象已经创建出来了)。

__init__(self, clsname, bases, clsdict)

在这里举个例子,如果我们想阻挠在类定义中包含大小写混用的方法名(驼峰命名)这种行为,可以编写以下元类:

class NoMixedCaseMeta(type):
    def __init__(self, clsname, bases, clsdict):
        for name in clsdict:
            if name.lower() != name:
                raise TypeError("Do not define MIXED-CASE Methods or attributes.")
        super().__init__(clsname, bases, clsdict)

我们在命令行定义两个类测试一下:

>>> class test(metaclass=NoMixedCaseMeta):
...     def foo_bar(self):
...         print("Hello")
...         
>>> class test2(metaclass=NoMixedCaseMeta):
...     def fooBar(self):
...         print("Hello")
...         
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "/home/jeza/PycharmProjects/PythonCookbook/metaclass_test.py", line 61, in __init__
    raise TypeError("Do not define MIXED-CASE Methods or attributes.")
TypeError: Do not define MIXED-CASE Methods or attributes.

可以看到,在创建类test2对象期间,因为在元类NoMixedCaseMeta的__init__()方法发现了类test2的一个方法fooBar混用了大小写,因此抛出了一个TypeError异常,阻止了类test2的创建。

至于在元类中定义__new__()还是__init__(),取决于我们打算如何使用得到的结果类。__new__()会在类创建之前先得到调用,当元类想以某种方式修改类的定义时(通过修改类字典中的内容,即前文的clsdict)一般会有这个方法。而__init__()方法会在类已经创建完成后才得到调用,如果想编写代码同完全成形(fully formed)的类对象打交道,那么重新定义__init__()方法会很有用。在这里,我们先不具体描述它们之间的区别。

3.3 __call__()方法

在上面我们可以看到,在元类中定义__new__()方法或者__init__()方法可以“干涉”类的创建过程,其两者区别在于“干涉“的时机(即__new__()方法调用时,类还没创建出来;而__init__()调用时,类已经创建出来了,该方法是对成形的类对象进行初始化或定制操作。从这两个方法的第一个参数可以看出两者的调用时机的不同)。那么,这一节所说的__call__()方法,究竟是什么东西呢?

我们清楚,如果一个类定义了__call__()方法,那么其创建出来的实例是可以像函数那样直接调用,即:

>>> class CallableTest:
...     def __init__(self, name):
...             self.name = name
...     def __call__(self, age):
...             print('{} is {} years old.'.format(self.name, age))
... 
>>> 
>>> a = CallableTest("jeza")
>>> a(21)
jeza is 21 years old.

那么,如果一个元类定义了一个__call__()方法,这就意味着通过该元类创建的类是可调用的——而类所谓的调用,其实就是类的构造函数。所以,通过重载元类的__call__()方法,我们就可以”干涉“类实例的创建过程。

在”2. 初窥元类“这一章节我们已经接触到__call__()方法了,在那个例子中,我们在实例创建之前去”干涉“它的过程,如果发现先前已经创建过实例_instance了,就直接返回_instance,从而绕过类的__init__()方法,实现单例模式。

那在这里我们探讨一下:实例创建的过程中,元类的__call__()方法,类的__new__()方法和__init__()方法之间的调用顺序。

第一种,通过传统的函数调用方式实例化一个类,它们之间的调用顺序。我们编写一下代码:

class Meta(type):
    def __call__(self, *args, **kwargs):
        print("Meta.__call__")
        return super().__call__(*args, **kwargs)  # 该语句千万不能漏


class A(metaclass=Meta):
    def __new__(cls, *args, **kwargs):
        print("A.__new__")
        return super().__new__(cls, *args, **kwargs)  # 该语句千万不能漏

    def __init__(self):
        print("A.__init__")


if __name__ == '__main__':
    a = A()

输出的结果如下:

Meta.__call__
A.__new__
A.__init__

可知,当使用函数调用的方式实例化一个类,会先调用元类的__call__()方法,随后再调用类的__new__()方法,最后再使用__init__()方法进行最后的实例化操作。

第二种,如果使用类的__new__()方法直接创建一个实例:

>>> a = A.__new__(A)
A.__new__

可知,使用类的__new__()方法不仅可以绕过了类的__new__()方法,还绕过了元类的__call__()方法。从中我们知道,使用函数调用的方式实例化一个类就相当于执行元类的魔术方法__call__()

4. 类的创建过程

要了解元类的作用,我们就需要了解 Python中类的创建过程 ,如下:

  1. 当Python见到class关键字时,会首先解析class ...中的内容。例如解析基类信息,最重要的是找到对应的元类信息(默认是type)。

  2. 元类找到后,Python 需要准备命名空间namespace(也可以认为是上节中typedict参数)。如果元类实现了 __prepare__ ()函数,则会调用它来得到默认的 namespace 。

  3. 之后是调用exec()来执行类的body,包括属性和方法的定义,最后这些定义会被保存进 namespace。

  4. 上述步骤结束后,就得到了创建类需要的所有信息,这时Python会调用元类的__new__()函数、__init__()函数来真正创建类。

如果你想在类的创建过程中做一些定制(customization)的话,创建过程中任何用到了元类的地方,我们都能通过覆盖元类的默认方法来实现定制。这也是元类“无所不能”的所在,它深深地嵌入了类的创建过程。

5. 聊聊__prepare__()函数

在第4节我们知道,元类的__prepare__()方法会在类定义一开始的时候立刻得到调用,且需要返回一个映射性对象(mapping object)供处理类定义体(body)时使用。

具体来说,__prepare__()为一个类方法(classmethod),调用时以类名和基类名称作为参数,即:

__prepare__(cls, clsname, bases)

那么,这个方法到底有什么具体应用情景呢?在一些情况下,我们可能序列化某些对象,序列化成csv表格等。此时我们可能想自动记录下属性和方法在类定义的顺序,这样就能利用这个顺序来完成csv表格的序列化操作。

相关代码如下所示:

from collections import OrderedDict


class Typed:
    _expected_type = type(None)

    def __init__(self, name=None):
        self._name = name

    def __set__(self, instance, value):
        if not isinstance(value, self._expected_type):
            raise TypeError('Expected ' + str(self._expected_type))
        instance.__dict__[self._name] = value


class Integer(Typed):
    _expected_type = int


class Float(Typed):
    _expected_type = float


class String(Typed):
    _expected_type = str


class OrderedMeta(type):
    def __new__(cls, clsname, bases, clsdict):
        d = dict(clsdict)
        order = []
        for name, value in clsdict.items():
            if isinstance(value, Typed):
                value._name = name
                order.append(name)
                d['_order'] = order
        return type.__new__(cls, clsname, bases, d)

    @classmethod
    def __prepare__(mcs, name, bases):
        return OrderedDict()


class Structure(metaclass=OrderedMeta):
    def as_csv(self):
        return ','.join(str(getattr(self, name)) for name in self._order)


class Stock(Structure):
    name = String()
    shares = Integer()
    price = Float()

    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

这里用到了描述器(descriptor),如果不太清楚描述器可以自行谷歌学习。

我们运行下解释器,看看结果:

>>> s = Stock("GOOG", 100, 49.1)
>>> s.name
'GOOG'
>>> s.as_csv()
'GOOG,100,49.1'
>>> s = Stock("AAPL", 'a lot', 9.22)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "/home/jeza/PycharmProjects/PythonCookbook/metaclass_test2.py", line 56, in __init__
    self.shares = shares
  File "/home/jeza/PycharmProjects/PythonCookbook/metaclass_test2.py", line 12, in __set__
    raise TypeError('Expected ' + str(self._expected_type))
TypeError: Expected <class 'int'>

在这里,我们可以看到as_csv方法可以严格按照类定义体各个属性的顺序输出成字符串的形式。原因就在于元类的__prepare__()方法返回了一个有序字典OrderDict的实例,因此类中各个属性间的顺序可以方便得到维护。这就是__prepare__()方法的重要用处之一。

值得注意的是,当创建最终的类对象的时候,我们需要在元类的__new__()方法中将这个字典转换成一个合适的dict实例才行,这就是为什么需要d = dict(clsdict)的原因。所以,__prepare__()方法放回的字典更多是充当一个中间的角色,用于将类定义体的各个属性和方法以自己所需的形式存储下来,并在__new__()方法中完成自己所需的操作。

6. 总结

  1. 对象的类型称为类,类的类型就称为元类。

  2. Python 中对元类实例化的结果就是“类”,这个过程是动态的。

  3. 在定义类时可以指定元类来”干涉“类的创建过程。