翻译自原文:https://realpython.com/python-metaclasses/

术语元编程是指程序了解或操纵自身的潜力。Python为类提供了元编程的一种形式叫元类

元类是一种深奥的OOP概念,几乎隐藏在所有Python代码中。无论你是否意识到,你都在使用元类。大多数时候,你也不需要意识到。大多数Python开发者很少必须考虑元类。

但是当有这样的需要时,Python提供了一种不是所有面向对象语言都支持的能力:你可以了解内部原理并且自定义元类。自定义元类的使用是有些争议的,正如下面引用Tim Peters的建议,他是Python领袖,编写了Python之禅

“元类比99%的用户担心的更有魔力。如果你想要知道你是否需要用他们,那你就不需要用(真正需要用的准确地知道他们需要,且不需要任何解释)。”

—— Tim Peters

有些Pythonistas(Python爱好者们被称为Pythonistas)认为你永远不应该用自定义元类。这可能有点过,但自定义元类大多不必要很可能是真的。如果一个问题不是很明显需要元类,那如果用一种更简单的方式解决,可能更清晰、更可读。

然而,理解Python元类是值得的,因为它更好地带领我们理解Python类的内部结构。你永远不知道会不会有一天发现自己就处在这样的境况中,只知道自定义元类就是你想要的。

旧式类 vs. 新式类

在Python中,一个类可以是两种类型之一。没有确定的官方术语,所以两种类型被非正式地称作旧式类和新式类。

旧式类

在旧式类中,类和类型是不同的。一个旧式类的实例总是实现于一个叫instance的单个内置类型。假设obj是一个旧式类的实例,obj.__class__指定为该类,但type(obj)总是instance。下面的例子取自Python 2.7:

1
2
3
4
5
6
7
8
>>> class Foo:
...     pass
...
>>> x = Foo()
>>> x.__class__
<class __main__.Foo at 0x000000000535CC48>
>>> type(x)
<type 'instance'>

新式类

新式类统一了类和类型的概念。假设obj是一个新式类的实例,obj.__class__type(obj)是相同的:

1
2
3
4
5
6
7
8
9
>>> class Foo:
...     pass
>>> obj = Foo()
>>> obj.__class__
<class '__main__.Foo'>
>>> type(obj)
<class '__main__.Foo'>
>>> obj.__class__ is type(obj)
True
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
>>> n = 5
>>> d = { 'x' : 1, 'y' : 2 }

>>> class Foo:
...     pass
...
>>> x = Foo()

>>> for obj in (n, d, x):
...     print(type(obj) is obj.__class__)
...
True
True
True

类型和类

Python3中,所有的类都是新式类。因此,在Python3中可以交替引用对象的类型和类。

Note: Python2中,类默认都是旧式的,Python2.2以前是完全不支持新式类的。从Python2.2开始,可以创建他们但必须显性地声明为新式的。

记住,Python中一切都是对象。类也是对象。因此,类必须有一个类型。一个类的类型是什么呢?

思考以下内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
>>> class Foo:
...     pass
...
>>> x = Foo()

>>> type(x)
<class '__main__.Foo'>

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

如你所见,x的类型是Foo类,但类自身Foo的类型是type。通常来说,所有新式类的类型是type

你所熟悉的内置类的类型也是type

1
2
3
4
5
6
7
8
>>> for t in int, float, dict, list, tuple:
...     print(type(t))
...
<class 'type'>
<class 'type'>
<class 'type'>
<class 'type'>
<class 'type'>

同样的,type的类型也是type(是的,是真的):

1
2
>>> type(type)
<class 'type'>

type是一个元类,它的类是实例。正如一个普通对象是一个类的实例,Python中任何新式类,以及Python3中的所有类,都是type元类的一个实例。

在上述案例中:

  • xFoo类的一个实例。
  • Footype元类的一个实例。
  • type也是type元类的一个实例,因此它是它自己的一个实例。

动态定义一个类

内置函数type()传递一个参数,返回一个对象的类型。对于新式类,一般和对象的__class__属性获取的一致:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
>>> type(3)
<class 'int'>

>>> type(['foo', 'bar', 'baz'])
<class 'list'>

>>> t = (1, 2, 3, 4, 5)
>>> type(t)
<class 'tuple'>

>>> class Foo:
...     pass
...
>>> type(Foo())
<class '__main__.Foo'>

你也可以用三个参数调用type()——type(<name>, <bases>, <dct>):

  • <name>指定类名。成为类的__name__属性。
  • <bases>指定类继承的基类元组。成为类的__bases__属性。
  • <dct>指定一个包含类主体定义的命名空间字典。成为类的__dict__属性

用这种方式调用type()创建type元类的新实例。换句话说,它动态创建了一个新的类。

在下面的每个示例中,上面的代码段用type()动态定义了一个类,下面的代码段用class语句最通用的方式定义类。每个示例这两个代码段在功能上是相等的。

示例1

第一个示例,传入type()<bases><dct>参数都为空,没有指定继承任何父类,命名空间字典中也没有初始放任何东西。这是尽可能最简单的类定义:

1
2
3
4
5
>>> Foo = type('Foo', (), {})

>>> x = Foo()
>>> x
<__main__.Foo object at 0x04CFAD50>
1
2
3
4
5
6
>>> class Foo:
...     pass
...
>>> x = Foo()
>>> x
<__main__.Foo object at 0x0370AD50>

示例2

这里<bases>是只有一个元素Foo的元组,指定Bar继承这个父类。属性attr被初始放在命名空间字典:

1
2
3
4
5
6
7
8
9
>>> Bar = type('Bar', (Foo,), dict(attr=100))

>>> x = Bar()
>>> x.attr
100
>>> x.__class__
<class '__main__.Bar'>
>>> x.__class__.__bases__
(<class '__main__.Foo'>,)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
>>> class Bar(Foo):
...     attr = 100
...

>>> x = Bar()
>>> x.attr
100
>>> x.__class__
<class '__main__.Bar'>
>>> x.__class__.__bases__
(<class '__main__.Foo'>,)

示例3

这一次<bases>再次为空,两个对象通过<dct>被放进命名空间字典。第一个是一个命名为attr的属性,第二个是命名为attr_val的函数,成为被定义类的一个方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
>>> Foo = type(
...     'Foo',
...     (),
...     {
...         'attr': 100,
...         'attr_val': lambda x : x.attr
...     }
... )

>>> x = Foo()
>>> x.attr
100
>>> x.attr_val()
100
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
>>> class Foo:
...     attr = 100
...     def attr_val(self):
...         return self.attr
...

>>> x = Foo()
>>> x.attr
100
>>> x.attr_val()
100

示例4

只有非常简单的函数可以用Python的lambda来定义。在下面的示例中,在外部定义一个略微复杂点的函数,在命名空间字典中分配给attr_val命名为f:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
>>> def f(obj):
...     print('attr =', obj.attr)
...
>>> Foo = type(
...     'Foo',
...     (),
...     {
...         'attr': 100,
...         'attr_val': f
...     }
... )

>>> x = Foo()
>>> x.attr
100
>>> x.attr_val()
attr = 100
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
>>> def f(obj):
...     print('attr =', obj.attr)
...
>>> class Foo:
...     attr = 100
...     attr_val = f
...

>>> x = Foo()
>>> x.attr
100
>>> x.attr_val()
attr = 100

自定义元类

再思考下这个旧示例:

1
2
3
4
>>> class Foo:
...     pass
...
>>> f = Foo()

表达式Foo()创建了一个类Foo的一个新实例。当解释器遇到Foo(),将发生下面情形:

  • Foo父类的__call__()方法被调用。因为Foo是一个标准的新式类,它的父类是type元类,所以type__call__()方法被调用。
  • __call__()方法依次调用下面方法:
    • __new__()
    • __init__()

如果Foo没有定义__new__()__init__(),默认方法是继承自Foo的祖先。但是如果Foo定义了这些方法,他们重写祖先的那些方法,允许在实例化Foo时自定义行为。

在下面的示例中,定义了一个自定义方法new(),并且分配给Foo作为__new__()方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
>>> def new(cls):
...     x = object.__new__(cls)
...     x.attr = 100
...     return x
...
>>> Foo.__new__ = new

>>> f = Foo()
>>> f.attr
100

>>> g = Foo()
>>> g.attr
100

这修改了类Foo的实例化行为:每次Foo的一个实例被创建,默认初始化一个叫attr的属性,并赋值100。(像这样的代码更常出现在__init__()方法里,不常在__new__()。这个示例是为了演示目的而设计。)

现在,正如已经重申的,类也是对象。假设你创建一个像Foo的类时,想要类似的自定义实例化行为。如果你遵循上面的模式,你要再定义一个自定义方法,并将其赋值给Foo是其实例的类的__new__()方法。Footype元类的一个实例,所以代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 剧透警告:这行不通!
>>> def new(cls):
...     x = type.__new__(cls)
...     x.attr = 100
...     return x
...
>>> type.__new__ = new
Traceback (most recent call last):
  File "<pyshell#77>", line 1, in <module>
    type.__new__ = new
TypeError: can't set attributes of built-in/extension type 'type'

但是,如你所见,你不能给type元类重新赋值__new__()方法。Python是不允许的。

这或许也没关系。type是派生所有新式类的元类,你的确不应该乱动它。但是有什么方式来自定义实例化一个类呢?

一个可能的方案是自定义元类。实际上你可以定义自己的元类,它派生自type,而不是搞乱type元类。

第一步是定义一个派生自type的元类,如下:

1
2
3
4
5
6
>>> class Meta(type):
...     def __new__(cls, name, bases, dct):
...         x = super().__new__(cls, name, bases, dct)
...         x.attr = 100
...         return x
...

定义头class Meta(type):指定Meta派生自type。因为type是一个元类,所以Meta也是一个元类。

注意到为Meta定义了一个自定义的__new__()方法,这是不可能为type元类这样定义的。这个__new__()方法做了如下的事:

  • 通过super()委托父元类(type)的__new__()方法创建一个新类
  • 给这个类分配自定义属性attr,并赋值100
  • 返回新创建的类

现在是巫毒的另一半:定义一个新类Foo并指定元类是自定义元类Meta,而不是标准元类type。这是像下面这样使用类定义中的metaclass完成的:

1
2
3
4
5
>>> class Foo(metaclass=Meta):
...     pass
...
>>> Foo.attr
100

看!Foo自动从Meta元类拿到了attr属性。当然,任何类似定义的其他类也能这样:

1
2
3
4
5
6
7
8
>>> class Bar(metaclass=Meta):
...     pass
...
>>> class Qux(metaclass=Meta):
...     pass
...
>>> Bar.attr, Qux.attr
(100, 100)

和类作为创建对象的模版作用一样,元类也作为创建类的模版发挥作用。元类有时被称为类工厂

对比下面两个示例:

对象工厂

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
>>> class Foo:
...     def __init__(self):
...         self.attr = 100
...

>>> x = Foo()
>>> x.attr
100

>>> y = Foo()
>>> y.attr
100

>>> z = Foo()
>>> z.attr
100

类工厂

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
>>> class Meta(type):
...     def __init__(
...         cls, name, bases, dct
...     ):
...         cls.attr = 100
...
>>> class X(metaclass=Meta):
...     pass
...
>>> X.attr
100

>>> class Y(metaclass=Meta):
...     pass
...
>>> Y.attr
100

>>> class Z(metaclass=Meta):
...     pass
...
>>> Z.attr
100

真的重要吗?

尽管上面的类工厂示例很简单,它是元类如果起作用的本质。他们允许自定义类的实例化。

不过,仅仅是把自定义属性attr赋予给每个新创建的类就需要大量的忙活。你真需要为此用元类吗?

在Python中,至少有几种其他方式可以有效地完成同样的事情:

简单的继承

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
>>> class Base:
...     attr = 100
...

>>> class X(Base):
...     pass
...

>>> class Y(Base):
...     pass
...

>>> class Z(Base):
...     pass
...

>>> X.attr
100
>>> Y.attr
100
>>> Z.attr
100

类装饰器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
>>> def decorator(cls):
...     class NewClass(cls):
...         attr = 100
...     return NewClass
...
>>> @decorator
... class X:
...     pass
...
>>> @decorator
... class Y:
...     pass
...
>>> @decorator
... class Z:
...     pass
...

>>> X.attr
100
>>> Y.attr
100
>>> Z.attr
100

结论

正如Tim Peters建议的,元类很容易变成“查询问题的解决方案”。通常不需要创建自定义元类。如果问题能用一种更简单的方式解决,就应该用简单的解决。尽管如此,理解元类还是有好处的,以便你能总体上理解Python类,并能意识到什么时候元类是真正合适的使用工具。