AberSheeran
Aber Sheeran

Python的元类

起笔自
所属文集: 程序杂记
共计 8163 个字符
落笔于

关于什么是元类(metaclass)、怎么用元类,我查了很多资料。中文的英文的都有,但是似乎都是讲理论,没有实际的给出一个整体的代码。

理论

几乎所有教程里都会谈到这一点:

类也是一个对象,元类就是创建这个对象的类。

参考metaclass的官方文档

By default, classes are constructed using type(). The class body is executed in a new namespace and the class name is bound locally to the result of type(name, bases, namespace).

所有类的默认元类就是type,并且它接受三个参数namebasesnamespace

  • name是一个str,它是类的名字,一般来说,在元类里不需要动它。
  • bases是一个tuple,它包含类的所有父类,如果没有父类,那么当然为空。
  • namespace是一个dict,它包含这个类定义里的所有内容——类里面的方法、属性,以及类所属的模块名,类的__qualname__(这里我不知道怎么翻译,一般来说这个值就是类名,用于类内的方法的__qualname__生成)。

实践

所以我们来实践一下,作为一个元类,它是如何作为的。以下是一个仅仅打印执行过程而不做其他操作的元类。

class MetaClass(type):
    """
    https://docs.python.org/3/reference/datamodel.html#metaclasses
    """
    def __new__(cls, name, bases, namespace):
        print("MetaClass.__new__ has be called")
        print(" -", name, bases, namespace)
        return super().__new__(cls, name, bases, namespace)

    def __init__(self, name, bases, namespace):
        print("MetaClass.__init__ has be called")
        print(" -", name, bases, namespace)

    def __call__(self, *args, **kwargs):
        print("MetaClass.__call__ has be called")
        print(" -", args, kwargs)
        return super().__call__(*args, **kwargs)

那么接下来使用这个元类试试。

class TestClass(metaclass=MetaClass):
    a = 123

    def __new__(cls, *args, **kwargs):
        print("TestClass.__new__ has be called")
        print(" -", args, kwargs)
        return super().__new__(cls)

    def __init__(self, *args, **kwargs):
        self.args = args
        self.kwargs = kwargs
        print("TestClass.__init__ has be called")
        print(" -", args, kwargs)

    def test(self):
        print("TestClass.test has be called")

执行结果如下:

MetaClass.__new__ has be called
 - TestClass () {'__module__': '__main__', '__qualname__': 'TestClass', 'a': 123, '__new__': <function TestClass.__new__ at 0x000002719E926BF8>, '__init__': <function TestClass.__init__ at 0x000002719E926C80>, 'test': <function TestClass.test at 0x000002719E926D08>, '__classcell__': <cell at 0x000002719E8C98E8: empty>}
MetaClass.__init__ has be called
 - TestClass () {'__module__': '__main__', '__qualname__': 'TestClass', 'a': 123, '__new__': <function TestClass.__new__ at 0x000002719E926BF8>, '__init__': <function TestClass.__init__ at 0x000002719E926C80>, 'test': <function TestClass.test at 0x000002719E926D08>, '__classcell__': <cell at 0x000002719E8C98E8: MetaClass object at 0x000002719E7CA468>}

可以看到,类在被加载的时候,就已经调用元类进行构造了。并且__new____init__的区别在于,在__new__被调用的返回值将作为类的实际定义,__init__没有返回值。
这就导致——如果你想更改类的定义,只能在__new__里做;如果想查看类的最终定义,那就在__init__里做。

可以尝试把MetaClass当中的__new__的返回值给删掉,它将返回一个默认的None。当你调用类的时候,你就会发现,类已经变成了None——无论它原本的定义是什么。

在这里我强调这一点只是想说明metaclass.__new__的权限很大,使用的时候一定要小心使用。当你不确定是否一定需要用它的时候,那就不要用它。

接下来执行下面的代码:

t = TestClass(123, 12, 1)
t.test()

执行结果如下:

MetaClass.__new__ has be called
 - TestClass () {'__module__': '__main__', '__qualname__': 'TestClass', 'a': 123, '__new__': <function TestClass.__new__ at 0x0000019C78F16BF8>, '__init__': <function TestClass.__init__ at 0x0000019C78F16C80>, 'test': <function TestClass.test at 0x0000019C78F16D08>, '__classcell__': <cell at 0x0000019C78EB98E8: empty>}
MetaClass.__init__ has be called
 - TestClass () {'__module__': '__main__', '__qualname__': 'TestClass', 'a': 123, '__new__': <function TestClass.__new__ at 0x0000019C78F16BF8>, '__init__': <function TestClass.__init__ at 0x0000019C78F16C80>, 'test': <function TestClass.test at 0x0000019C78F16D08>, '__classcell__': <cell at 0x0000019C78EB98E8: MetaClass object at 0x0000019C76ECAE48>}
MetaClass.__call__ has be called
 - (123, 12, 1) {}
TestClass.__new__ has be called
 - (123, 12, 1) {}
TestClass.__init__ has be called
 - (123, 12, 1) {}
TestClass.test has be called

你会发现元类的__call__方法先于类的初始化方法被调用。

试试把__call__的返回给删掉,会有如下的执行结果

MetaClass.__new__ has be called
 - TestClass () {'__module__': '__main__', '__qualname__': 'TestClass', 'a': 123, '__new__': <function TestClass.__new__ at 0x0000026D8FF16BF8>, '__init__':
<function TestClass.__init__ at 0x0000026D8FF16C80>, 'test': <function TestClass.test at 0x0000026D8FF16D08>, '__classcell__': <cell at 0x0000026D8FEB98E8: empty>}
MetaClass.__init__ has be called
 - TestClass () {'__module__': '__main__', '__qualname__': 'TestClass', 'a': 123, '__new__': <function TestClass.__new__ at 0x0000026D8FF16BF8>, '__init__':
<function TestClass.__init__ at 0x0000026D8FF16C80>, 'test': <function TestClass.test at 0x0000026D8FF16D08>, '__classcell__': <cell at 0x0000026D8FEB98E8: MetaClass object at 0x0000026D8DF6B188>}
MetaClass.__call__ has be called
 - (123, 12, 1) {}
Traceback (most recent call last):
  File "c:/Users/AberS/Desktop/metaclass.py", line 39, in <module>
    t.test()
AttributeError: 'NoneType' object has no attribute 'test'

此时类的__new____init__都没有被执行,而__new__不被执行的后果就是:t = None

总结

Python中类的元类能够控制类的定义、以及类创建对象的过程。

  1. 定义类的代码执行时:

    首先调用元类的构造方法构造类。在__new__可读写类的定义;在__init__中只读,但能获取类最终的定义。

  2. 由类创建对象时:

    首先调用元类的__call__方法。由__call__调用类的构造方法创建对象。

实际运用

单例类

元类的__init__方法与__call__方法中的第一个参数理所当然是当前元类的实例化后产生的对象,也就是被控制的类。藉由此,可以使用元类创建单例类

class Singleton(type):

    def __init__(cls, name, bases, namespace):
        cls.instance = None

    def __call__(cls, *args, **kwargs):
        if cls.instance is None:
            cls.instance = super().__call__(*args, **kwargs)
        return cls.instance

使用函数作为元类

元类并不总是需要一个类去作为定义,很多时候可能使用函数就行——譬如你不需要控制类构造对象的过程时。

实际上元类只需要是一个可调用对象(Callable)并且返回type(name, bases, namespace)就行。

看下面一个例子:

def MetaFunc(name, bases, namespace):
    print("MetaFunc has be called")
    print(" -", name, bases, namespace)
    return type(name, bases, namespace)


class TestClass(metaclass=MetaFunc):
    a = 123

    def __new__(cls, *args, **kwargs):
        print("TestClass.__new__ has be called")
        print(" -", args, kwargs)
        return super().__new__(cls)

    def __init__(self, *args, **kwargs):
        self.args = args
        self.kwargs = kwargs
        print("TestClass.__init__ has be called")
        print(" -", args, kwargs)

    def test(self):
        print("TestClass.test has be called")


t = TestClass(123, 12, 1)
t.test()

执行结果如下:

MetaFunc has be called
 - TestClass () {'__module__': '__main__', '__qualname__': 'TestClass', 'a': 123, '__new__': <function TestClass.__new__ at 0x000001B0650F6C80>, '__init__': <function TestClass.__init__ at 0x000001B0650F6D08>, 'test': <function TestClass.test at 0x000001B0650F6D90>, '__classcell__': <cell at 0x000001B0650998E8: empty>}
TestClass.__new__ has be called
 - (123, 12, 1) {}
TestClass.__init__ has be called
 - (123, 12, 1) {}
TestClass.test has be called

传参给元类

在 ORM 之类的场景里,可能会需要传递参数给元类。

class MetaClass(type):
    """
    https://docs.python.org/3/reference/datamodel.html#metaclasses
    """

    def __new__(cls, name, bases, namespace, *, model):
        print("MetaClass.__new__ has be called")
        print(" -", name, bases, namespace, model)
        return super().__new__(cls, name, bases, namespace)

    def __init__(self, name, bases, namespace, *, model):
        print("MetaClass.__init__ has be called")
        print(" -", name, bases, namespace, model)


class TestClass(metaclass=MetaClass, model="T"):
    pass
如果你觉得本文值得,不妨赏杯茶
Python文件的热重载
加速Python的asyncio