Python 里的 metaclass 介绍

2,375 阅读12分钟

类(Classes) 也是对象(Objects)

在我们去了解 metaclass 之前,我们需要掌握 Python 里的类,在多数语言里,类就只是一些用来描述如何创建对象的代码,这在 Python 里也差不多一样:

>>> class ObjectCreator(object):
...       pass
...

>>> my_object = ObjectCreator()
>>> print(my_object)
<__main__.ObjectCreator object at 0x8974f2c>

但 Python 里的类还不仅仅是这些,在 Python 里,类也是对象。

没错,也是对象。

当你使用关键字 class的时候,Python 通过运行它然后创建了一个对象,这些语句:

>>> class ObjectCreator(object):
...       pass
...

在内存里创建了一个名为 "ObjectCreator" 的对象。

这个类对象,可以用来创建一个普通对象,而这个能力就让它成为了一个类。

但它依然是一个对象,因此:

你可以把它赋值给一个变量 可以复制它 可以给它增加属性 可以把它当作一个参数传给一个函数

>>> print(ObjectCreator) # 你可以打印这个类,因为它是一个对象
<class '__main__.ObjectCreator'>
>>> def echo(o):
...       print(o)
...
>>> echo(ObjectCreator) # 可以把它当参数传递
<class '__main__.ObjectCreator'>
>>> print(hasattr(ObjectCreator, 'new_attribute'))
False
>>> ObjectCreator.new_attribute = 'foo' # 可以给它增加属性
>>> print(hasattr(ObjectCreator, 'new_attribute'))
True
>>> print(ObjectCreator.new_attribute)
foo
>>> ObjectCreatorMirror = ObjectCreator # 可以赋值
>>> print(ObjectCreatorMirror.new_attribute)
foo
>>> print(ObjectCreatorMirror())
<__main__.ObjectCreator object at 0x8997b4c>

动态创建一个类

既然类也是对象,那我们就可以像对象一样动态创建它。

一种方式:我们可以在函数里使用 class关键字:

>>> def choose_class(name):
...     if name == 'foo':
...         class Foo(object):
...             pass
...         return Foo # 返回类,而不是对象
...     else:
...         class Bar(object):
...             pass
...         return Bar
...
>>> MyClass = choose_class('foo')
>>> print(MyClass) # 方法返回了一个类,而不是一个实例
<class '__main__.Foo'>
>>> print(MyClass()) # 可以用这个类来创建一个对象
<__main__.Foo object at 0x89c6d4c>

但它看上去并没有那么"动态",毕竟我们还是把所有代码都写下来了。

既然类也是对象,那它肯定是被什么东西生成的。

当我们使用 class关键字的时候,Python 自动创建了这个对象,但也像 Python 里其它东西一样,它提供了一个手动创建的办法。

还记得 type函数么,它可以让我们知道一个对象是什么类型的:

>>> print(type(1))
<type 'int'>
>>> print(type("1"))
<type 'str'>
>>> print(type(ObjectCreator))
<type 'type'>
>>> print(type(ObjectCreator()))
<class '__main__.ObjectCreator'>

type 还有一个新的功能,它可以让我们动态地创建类。type函数可以通过传一些参数来描述一个类,然后返回一个类对象:

type([类名],
      [包含父类的 tuple,用于继承,可为空],
      [字典对象,包含属性名和对应的值])

举个例子:

>>> class MyShinyClass(object):
...       pass

可以通过手工的方式这样创建:

>>> MyShinyClass = type('MyShinyClass', (), {}) # 返回类对象
>>> print(MyShinyClass)
<class '__main__.MyShinyClass'>
>>> print(MyShinyClass()) # 用这个类对象创建一个实例
<__main__.MyShinyClass object at 0x8997cec>

可以注意到我们用的是 "MyShinyClass" 作为类名,同时也是被赋值的变量名,它可以不一样。这里是为了不让大家弄混淆。

type 通过接收一个字典来定义属性,因此:

>>> class Foo(object):
...       bar = True

可以被写作:

>>> Foo = type('Foo', (), {'bar':True})

然后正常使用它:

>>> print(Foo)
<class '__main__.Foo'>
>>> print(Foo.bar)
True
>>> f = Foo()
>>> print(f)
<__main__.Foo object at 0x8a9b84c>
>>> print(f.bar)
True

当然我们也可以实现继承:

>>>   class FooChild(Foo):
...         pass

可以写成:

>>> FooChild = type('FooChild', (Foo,), {})
>>> print(FooChild)
<class '__main__.FooChild'>
>>> print(FooChild.bar) # bar 变量继承自 Foo
True

有时候我们也会给类上定义函数,我们也可以实现:

>>> def echo_bar(self):
...       print(self.bar)
...
>>> FooChild = type('FooChild', (Foo,), {'echo_bar': echo_bar})
>>> hasattr(Foo, 'echo_bar')
False
>>> hasattr(FooChild, 'echo_bar')
True
>>> my_foo = FooChild()
>>> my_foo.echo_bar()
True

我们当然也可以在创建了类对象之后增加更多的函数,就像给对象增加属性一样:

>>> def echo_bar_more(self):
...       print('yet another method')
...
>>> FooChild.echo_bar_more = echo_bar_more
>>> hasattr(FooChild, 'echo_bar_more')
True

通过这些你可以看到,在 Python 里类也是对象,我们可以动态地创建类

这就是当我们用 class关键字的时候,Python 做的事情,而它是通过 metaclass 的方式来完成的。

什么是 metaclass

metaclass 就是用来创建类的那些东西。

我们通过定义类来为之后创建对象,对吧?

然后我们又知道了 Python 里类也是对象。

那么,metaclass 就是用来创建这些类对象的东西,它们是类对象的类,你可以这样想象:

MyClass = MetaClass()
my_object = MyClass()

我们已经见过如何用 type来这样创建类了:

MyClass = type('MyClass', (), {})

那是因为type也是一个metaclass。type就是 Python 里用来创建所有类的 metaclass

那么你一定奇怪为什么它是用小写的,而不是 Type

我想那是因为它为了和 str保持一致性, str 用来创建字符串对象, int用来创建整型对象, type用来创建类对象。

你可以通过 __class__属性看出来。

所有的东西,在 Python 里都是一个对象,包括整数,字符串,方法以及类。这些东西都是对象,这些对象都通过一个类来创建:

>>> age = 35
>>> age.__class__
<type 'int'>
>>> name = 'bob'
>>> name.__class__
<type 'str'>
>>> def foo(): pass
>>> foo.__class__
<type 'function'>
>>> class Bar(object): pass
>>> b = Bar()
>>> b.__class__
<class '__main__.Bar'>

那么,这些 __class_____class__又是什么呢?

>>> age.__class__.__class__
<type 'type'>
>>> name.__class__.__class__
<type 'type'>
>>> foo.__class__.__class__
<type 'type'>
>>> b.__class__.__class__
<type 'type'>

所以, metaclass 就是用来创建这些类对象的东西。

如果你愿意,可以叫它"类工厂"。

type 就是 Python 内置的 metaclass, 当然,我们也可以创建我们自己的 metaclass。

metaclass 属性

在 Python 2 里,我们可以在写一个类的时候增加 __metaclass__属性:

class Foo(object):
    __metaclass__ = something...
    [...]

如果我们这样做了, Python 就会用这个 metaclass 来创建Foo类。

在这里要注意:

你先是写了 class Foo(object),但是 Foo 的类对象还没在内存里创建(想一想我们第一个动态创建类的例子)。

Python 先是在类的定义里查找 __metaclass__,如果找到了,就用它来创建 Foo 这个类对象。如果没找到,就用 type来创建这个类对象

再描述一下:

当你这样写的时候:

class Foo(Bar):
    pass

Python 如下查找:

是否在Foo里有一个 __metaclass__属性?

如果是的,那就先用 __metaclass__在内存里创建一个类对象,然后给一个名字 Foo

如果Python找不到 __metaclass__属性,它就会在模块级别查找是否有 __metaclass__,然后尝试用同样的办法来创建类对象。

如果还是找不到 __metaclass__, Python 就会使用 Bar(第一个父类)的 metaclass (可能就是 type)来创建。

但注意,这里的 Foo 的 __metaclass__不会继承下来,而只会是是父类的 __class__,也就是说

Foo.__metaclass__ = Bar.__class__

而不是

Foo.__metaclass__ = Bar.__metaclass__ #错误

现在的问题就是,我们可以把什么放在 __metaclass__里?

回答是:只要可以创建一个类对象的就行。

那什么是可以创建类对象的呢? type可以,所有使用 type的子类也都可以。

Python 3 里的 metaclass

在 Python 3 里的写法换成如下:

class Foo(object, metaclass=something):
    ...

注:__metaclass__变量已经不再使用。

metaclass 的行为大体没变

只有一点区别是,在 Python 3 里,我们可以通过命名参数来传递一些属性:

class Foo(object, metaclass=something, kwarg1=value1, kwarg2=value2):
    ...

自定义 metaclass

使用 metaclass 最主要的目的就是为了在创建类的时候动态改变类。

我们通常会用在定义一些 API 上,比方说根据当前上下文来创建不同的类

想象这么一个例子,我们希望自己的一个模块里所有的类的所有变量都是大写字母。 有很多种方式可以实现这一点,其中一种方式就是使用模块级别的 __metaclass__

通过这种方式,所有在这个模块里的类都由这个 metaclass 创建,我们只需要告诉 metaclass 把所有属性都变成大写就可以了。

幸运的是, __metaclass__可以是任意的可被调用的东西,它不一定需要是一个类。

所以,我们使用函数来完成这个小例子

# metaclass 会像 type 函数一样接收到这些参数
def upper_attr(future_class_name, future_class_parents, future_class_attr):

    # 只要不是 '__' 开头的属性,就全转为大写
    uppercase_attr = {}
    for name, val in future_class_attr.items():
        if not name.startswith('__'):
            uppercase_attr[name.upper()] = val
        else:
            uppercase_attr[name] = val

    # 然后用 `type` 来完成接下来的工作
    return type(future_class_name, future_class_parents, uppercase_attr)

__metaclass__ = upper_attr # 放在模块级别上,这会影响所有这个模块下的类

class Foo():
    bar = 'bip'

print(hasattr(Foo, 'bar'))
# Out: False
print(hasattr(Foo, 'BAR'))
# Out: True

f = Foo()
print(f.BAR)
# Out: 'bip'

现在,我们用一个类来实现 metaclass:

# type 就像一个类一样,所以我们可以继承自它
class UpperAttrMetaclass(type):
    # __new__ 会在 __init__ 方法之前被调用
    # 它会用来创建对象并返回,而 __init__ 会初始化这个对象的属性
    # 通常我们很少使用 __new__,除非我们要控制对象的创建过程
    # 这里通过类来创建对象,我们想要自定义一些流程,所以我们覆盖 __new__
    # 如果想要的话,也可以在 __init__ 里做这些事情
    def __new__(upperattr_metaclass, future_class_name,
                future_class_parents, future_class_attr):

        uppercase_attr = {}
        for name, val in future_class_attr.items():
            if not name.startswith('__'):
                uppercase_attr[name.upper()] = val
            else:
                uppercase_attr[name] = val

        return type(future_class_name, future_class_parents, uppercase_attr)

但这还不够"面向对象",我们直接通过调用 type 而不是通过父类的 __new__,让我们修改一下:

class UpperAttrMetaclass(type):

    def __new__(upperattr_metaclass, future_class_name,
                future_class_parents, future_class_attr):

        uppercase_attr = {}
        for name, val in future_class_attr.items():
            if not name.startswith('__'):
                uppercase_attr[name.upper()] = val
            else:
                uppercase_attr[name] = val

        # 使用父类 type.__new__ 方法
        return type.__new__(upperattr_metaclass, future_class_name,
                            future_class_parents, uppercase_attr)

你可能注意到这里有一个额外的参数 upperattr_metaclass。这不是什么特别的东西,__new__函数会接受它即将定义的这个类作为第一个参数,就像我们在定义类方法的时候每个函数都有一个 self对象一样,因为这里是定义类,所以参数传的是一个类对象。

当然,这里所有的参数名都用的比较长,主要是为了方便说明,在实际使用的时候我们可以这样:

class UpperAttrMetaclass(type):

    def __new__(cls, clsname, bases, dct):

        uppercase_attr = {}
        for name, val in dct.items():
            if not name.startswith('__'):
                uppercase_attr[name.upper()] = val
            else:
                uppercase_attr[name] = val

        return type.__new__(cls, clsname, bases, uppercase_attr)

我们通过使用 super关键字可以更清晰一些,当然也更容易继承:

class UpperAttrMetaclass(type):

    def __new__(cls, clsname, bases, dct):

        uppercase_attr = {}
        for name, val in dct.items():
            if not name.startswith('__'):
                uppercase_attr[name.upper()] = val
            else:
                uppercase_attr[name] = val

        return super(UpperAttrMetaclass, cls).__new__(cls, clsname, bases, uppercase_attr)

在 Python 3 里我们还有一些命名参数:

class Foo(object, metaclass=Thing, kwarg1=value1):
    ...

它会被转化成这样的调用:

class Thing(type):
    def __new__(cls, clsname, bases, dct, kwargs1=default):
        ...

就这样,metaclass 没有什么更多更特别的东西了。

metaclass 背后的复杂性通常不是 metaclass 本身,而是我们在使用 metaclass 的时候通常在修改一些内部的东西,调整一下继承什么的。这些东西是容易让人迷糊的,但 metaclass 是很简单的一些东西:

  • 介入类的创建过程
  • 修改类的一些属性
  • 返回修改之后的类

为什么用 metaclass 而不是函数来创建类

既然 __metaclass__可以是任意可被调用的东西,那为什么我们使用一个类而不是方法来实现呢?

有这么一些原因:

  • 我们的目的更明确。让别人看到 UpperAttrMetaclass(type) 的时候,他们很清楚知道这是在调整类。
  • 我们可以使用面向对象的技术, metaclass 可以继承自另一个 metaclass,覆盖父类的方法,甚至 metaclass 还可以使用 metaclass 来创建。
  • 一个类的子类可以被父类的 metaclass 创建,而如果使用方法的方式来实现 metaclass 就不行了。
  • 我们可以更好地结构化我们的代码,当我们定义一个 metaclass 的时候,我们就可以把相关的东西放进来,而用方法的话通常会把东西散落在各处。
  • 我们可以监听不同的创建阶段,比方说 __new____init__或者 __call__,这可以让我们做不同的事情。
  • 最后,毕竟它们可是叫 metaclass 啊。总得像个类吧!

为什么要使用 metaclass

这可是个大问题,为什么要使用这些容易出错的牛逼功能?

事实上,通常情况下你不需要使用:

Metaclasses are deeper magic that 99% of users should never worry about. If you wonder whether you need them, you don't (the people who actually need them know with certainty that they need them, and don't need an explanation about why).

来自 Python 大师:Tim Peters

使用 metaclass 的一个主要场景是创建一些 API。一个经典的例子就像是 Django ORM 框架。

它允许你这样定义一些东西:

class Person(models.Model):
    name = models.CharField(max_length=30)
    age = models.IntegerField()

但如果你这样使用:

guy = Person(name='bob', age='35')
print(guy.age)

它就不会返回 IntegerField 对象了,而是直接一个 int 值,它甚至还可以直接接收数据库里的数据格式。

这可能是因为 models.Model 定义了 __metaclass__然后使用这些技巧把我们刚才定义的 Person 类的属性增加了一些钩子函数,然后就可以处理各种不同的赋值了。

Django 通过使用 metaclass 的方式把一些复杂的东西做的使用起来很简单,但实际上在背后做了很多工作。

最后总结一下

起先,我们知道了类也是对象,只是它们是用于创建实例的。

事实上,他们也是一些实例,metaclass 的实例。

>>> class Foo(object): pass
>>> id(Foo)
142630324

在 Python 里,所有的东西都是对象,他们要么是类的实例,要么是 metaclass 的实例。

除了 type

type是它自己的 metaclass。这玩意不是我们能在纯 Python 语言内能够实现的。

然后,metaclass 是复杂的,在类的简单的一些调整中我们可能不太想用 metaclass 来实现,我们可以用另两种技术来改变:

  • 直接增加属性这类的补丁做法
  • 对类使用装饰器模式

99% 的情况下,这两种方式就足够了。

而通常98%的情况下,我们都根本不需要去调整类。

我的blog