< 返回博客

Python中的元类概念浅析


“元类就是深度的魔法,99%的用户应该根本不必为此操心。如果你想搞清楚究竟是否需要用到元类,那么你就不需要它。那些实际用到元类的人都非常清楚地知道他们需要做什么,而且根本不需要解释为什么要用元类。”
—— Python界的领袖 Tim Peters

疑问

在使用SqlAlchemy框架的时候,总是奇怪,这样的代码是如何运行的:

class User(Base):
    id = Column(Integer, primary_key=True)
    uid = Column(Integer)
    uname = Column(String(30))
    ...

用户写出类似这样的代码,框架需要根据这个类,创建出外部对应的数据表,或者去适配一个数据表,那么问题就在这里:User类如何知道自己所拥有的Column类的信息?

在Python中,没有魔法方法可以使得Column实例化的时候主动向User类注册信息,那么只能是User类的作用了。而User类要实现这个功能,我想到使用dir函数来遍历类的所有属性,寻找Column类并记录:

class Base():
    ...
    __columns__ = []
    @classmethod
    def create_all(cls):
        for subclass in cls.__subclasses__():
            for x in dir(subclass):
                if not x.startswith("__") and isinstance(getattr(subclass, x), Column):
                    subclass.__columns__.append(x)

按理来说,这样的方式也可以,但当我查看SqlAlchemy的源码的时候,发现ORM这部分全是metaclass这样的东西,看得我是一脸懵逼。于是乎,谷歌上查了一通,这不查不知道,一查才知道,object原来不是Python的一等公民啊。

思考

老子云(笑):道生一,一生二,二生三,三生万物。宇宙中纷繁复杂的所有事物,都只是同一种更高层次的事物的不同表现形式,这与近现代物理的发现不谋而合,爱因斯坦晚年研究的大一统理论,不就是在寻找那个“道”嘛。所以不是生活太复杂,而是你站的不够高,格局不够大。呃,扯远了。。

在编程语言中,也有着类似的道理,OOP中的继承就是最经典的例子。其实这也很好理解,本来OOP就是模拟现实世界来着。在众多面向对象的语言中,一个类就是内存中“描述”了一个数据结构的数据结构,对类的实例化,就是生成一个用来描述具体数据的数据结构,虽然有点绕口,但就是那么回事。静态语言中的类是在编译前就固定好的,运行时直接载入内存,这也就导致了类的创建没有动态性。所以在一般的静态语言中,各种类就是道,没有一二三,使用类生成其他的实例。

而在Python中,“一切皆是对象”,包括类。
想想一般的对象我们是怎样创建的:

l = list()
d = dict()
t = tuple()

这种方式方便又符合直觉:使用类似调用函数的方式使用一个类,就可以创建一个类实例。但是我们一般会使用类似这样一个代码段创建一个类:

class Cat(Animal):
    name = 'cat 1'
    age = 5

    def say():
        print('miao')
    ...

类也是对象,可不可以使用类似创建一般对象的方式创建类呢?答案是可以。上述一段代码可以被翻译为:

Cat = type('Cat', (Animal,), { 'name': 'cat 1', 'age': 5, 'say': lambda self: print('miao')})

没错,除了查看对象的类型,type亦可以这样使用,用来动态地创建一个类。它的用法是这样的

type(name, bases, attrs)

其中,name是创建类的类名,bases为父类元组,attrs为属性字典。这种方式创建的类,与class关键字创建的毫无区别,

>>> Cat = type('Cat', (Animal,), { 'name': 'cat 1', 'age': 5, 'say': lambda self: print('miao')})
>>> print(Cat)
>>> print(Cat)
<class '__main__.Cat'>
>>> Cat()
<__main__.Cat object at 0x7fdede584588>
>>> c = Cat()
>>> c.say()
miao

这么看来,class关键字反倒是像个语法糖了。

type

那么,可以创建类的这个type(并非查看类型的那个单纯的type)是何方神圣呢?
单纯用来查看对象类型,type看起来像是一个普通的函数,很不起眼,像是__class__属性的魔法方法,但是它是一个类:

>>> type(type)
<class 'type'>

其实,它就是我们要说的元类,可以对它进行实例化,来创建出一个类。元类,所谓“元”,就是起始、开端的意思,顾名思义,一切对象的开端就是元类。看来type不仅仅是个看起来像函数的类,还是个返璞归真的终极大Boss,是万恶之源。

我们现在构建了这样一幅图景:

[type,元类对象] –实例化–> [list, dict之流,类对象] –实例化–> [instance,实例对象]

另外,元类既然也是一个类,那么它是谁的实例呢?考虑再出现“元元类”、“元元元类”并不科学,刻意地保持哲学性而使得元类是所有子类的实例也不现实,所以,元类也就是他自身的实例了。

元类的继承

既然元类是类对象,那么就可以被继承。假如我们要实现一个类,这个类中所有的list属性的属性名都为大写,那么可以这么写:

class MetaClass(type):
    def __new__(cls, name, bases, attrs):
        lists = [l for l in attrs if isinstance(attrs[l], list)]
        attrs.update({k.upper(): v for k, v in attrs.items()})
        for x in lists:
            attrs.pop(x)

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

class A(metaclass=MetaClass):
    la = [1, 2, 3]

print(A().la) # AttributeError
print(A().LA) # [1, 2, 3]

type可以被继承,继承时必须重载__new__函数;另外如果要指定一个类的元类,可以在类名后的括号内指定,class A(metaclass=MetaClass)。
在上面的例子中,元类截获了类创建的过程,改变了类的属性信息,而类对象对此一无所知。

总结

对有点OOP基础的人来书,元类理解起来并不难,但是难就难在对它的使用上,因为如果你遇到了只有元类才能解决的问题时,这个问题已经足够复杂了。

对于Python的一般用户来说,元类更像是为了补全Python的“世界观”,使Python逻辑自洽而存在的,有了元类,Python就真正可以达到“一切皆是对象”的程度了。

如果想看看别人手里的元类,建议看看类似flask-wtf这类简单点的,直接去看SQLAlchemy这类框架,离看到自闭也不远了。