geekdoc-python-zh/docs/realpython/python-metaclasses.md

14 KiB
Raw Permalink Blame History

Python 元类

原文:https://realpython.com/python-metaclasses/

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

元类是一个深奥的 OOP 概念,隐藏在几乎所有 Python 代码的背后。不管你是否意识到,你都在使用它们。在大多数情况下,你不需要意识到这一点。大多数 Python 程序员很少考虑元类。

然而当需要时Python 提供了一种并非所有面向对象语言都支持的功能:您可以在幕后定义自定义元类。自定义元类的使用有些争议,正如 Tim Peters(Python 大师,撰写了 Python 的 Zen)所说:

“元类比 99%的用户应该担心的还要神奇。如果你想知道你是否需要他们,你不需要(实际需要他们的人肯定知道他们需要他们,并且不需要关于为什么的解释)。”

蒂姆·彼得斯

有些 Python 爱好者(众所周知的 Python 爱好者)认为不应该使用自定义元类。这可能有点过了,但是自定义元类在大多数情况下是不必要的,这可能是真的。如果一个问题不太明显需要它们,那么如果用一种更简单的方式来解决,它可能会更干净,更易读。

尽管如此,理解 Python 元类是值得的,因为它通常会导致对 Python 类内部的更好理解。您永远不知道:有一天,您可能会发现自己处于这样一种情况,您只知道自定义元类是您想要的。

**获得通知:**不要错过本教程的后续— 点击这里加入真正的 Python 时事通讯你会知道下一期什么时候出来。

旧式与新式课堂

在 Python 领域,类可以是两种类型中的一种。官方术语尚未确定,因此它们被非正式地称为旧式和新式类。

Remove ads

旧式班级

对于旧式的类,类和类型不是一回事。旧式类的实例总是由一个名为instance的内置类型实现。如果obj是一个旧式类的实例,obj.__class__指定该类,但是type(obj)总是instance。以下示例摘自 Python 2.7:

>>> class Foo:
...     pass
...
>>> x = Foo()
>>> x.__class__
<class __main__.Foo at 0x000000000535CC48>
>>> type(x)
<type 'instance'>

新型班级

新型类统一了类和类型的概念。如果obj是一个新型类的实例,type(obj)obj.__class__相同:

>>> class Foo:
...     pass
>>> obj = Foo()
>>> obj.__class__
<class '__main__.Foo'>
>>> type(obj)
<class '__main__.Foo'>
>>> obj.__class__ is type(obj)
True
>>> 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

类型和等级

在 Python 3 中,所有的类都是新型类。因此,在 Python 3 中,互换引用对象的类型和类是合理的。

**注意:**在 Python 2 中,类默认是旧式的。在 Python 2.2 之前,根本不支持新型类。从 Python 2.2 开始,可以创建它们,但必须显式声明为 new-style。

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

请考虑以下情况:

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

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

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

如你所料,x的类型是类Foo。但是Foo的类型,职业本身,是type。一般来说,任何新型类的类型都是type

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

>>> for t in int, float, dict, list, tuple:
...     print(type(t))
...
<class 'type'>
<class 'type'>
<class 'type'>
<class 'type'>
<class 'type'>

就此而言,type的类型也是type(是的,真的):

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

是一个元类其中的类是实例。正如普通对象是一个类的实例一样Python 中的任何新型类,以及 Python 3 中的任何类,都是type元类的实例。

在上述情况下:

  • x是类Foo的一个实例。
  • Footype元类的一个实例。
  • type也是type元类的一个实例,所以它是自身的一个实例。

Python class chainRemove ads

动态定义一个类

当传递一个参数时,内置的type()函数返回一个对象的类型。对于新型类,这通常与对象的__class__属性相同:

>>> 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__属性。
  • <dct>指定一个包含类体定义的名称空间字典。这成为了类的__dict__属性。

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

在下面的每个例子中,上面的代码片段用type()动态定义了一个类,而下面的代码片段用class语句以通常的方式定义了这个类。在每种情况下,这两个片段在功能上是等效的。

示例 1

在第一个例子中,传递给type()<bases><dct>参数都是空的。没有指定来自任何父类的继承,并且最初在名称空间字典中没有放置任何东西。这是最简单的类定义:

>>> Foo = type('Foo', (), {})

>>> x = Foo()
>>> x
<__main__.Foo object at 0x04CFAD50>
>>> class Foo:
...     pass
...
>>> x = Foo()
>>> x
<__main__.Foo object at 0x0370AD50>

示例 2

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

>>> Bar = type('Bar', (Foo,), dict(attr=100))

>>> x = Bar()
>>> x.attr
100
>>> x.__class__
<class '__main__.Bar'>
>>> x.__class__.__bases__
(<class '__main__.Foo'>,)
>>> 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的函数,它成为定义的类的方法:

>>> Foo = type(
...     'Foo',
...     (),
...     {
...         'attr': 100,
...         'attr_val': lambda x : x.attr
...     }
... )

>>> x = Foo()
>>> x.attr
100
>>> x.attr_val()
100
>>> class Foo:
...     attr = 100
...     def attr_val(self):
...         return self.attr
...

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

示例 4

Python 中的 lambda只能定义非常简单的函数。在下面的例子中,一个稍微复杂一点的函数在外部定义,然后在名称空间字典中通过名字f赋给attr_val:

>>> 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
>>> def f(obj):
...     print('attr =', obj.attr)
...
>>> class Foo:
...     attr = 100
...     attr_val = f
...

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

Remove ads

自定义元类

再次考虑这个老生常谈的例子:

>>> 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__()方法:

>>> 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的类时类似地定制实例化行为。如果您遵循上面的模式,您将再次定义一个自定义方法,并将其指定为类的__new__()方法,而Foo是该类的一个实例。Footype元类的一个实例,所以代码看起来像这样:

# Spoiler alert:  This doesn't work!
>>> 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派生的元类,如下所示:

>>> 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关键字完成的,如下所示:

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

瞧! Foo已经从Meta元类中自动选取了attr属性。当然,您以类似方式定义的任何其他类也会这样做:

>>> class Bar(metaclass=Meta):
...     pass
...
>>> class Qux(metaclass=Meta):
...     pass
...
>>> Bar.attr, Qux.attr
(100, 100)

与类作为创建对象的模板一样,元类也作为创建类的模板。元类有时被称为类工厂

比较以下两个例子:

对象工厂:

>>> class Foo:
...     def __init__(self):
...         self.attr = 100
...

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

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

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

类工厂:

>>> 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

Remove ads

这真的有必要吗?

尽管上面的类工厂例子很简单,但它是元类工作的本质。它们允许定制类实例化。

尽管如此,仅仅是给每个新创建的类赋予自定义属性attr就太麻烦了。为此,您真的需要一个元类吗?

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

简单继承:

>>> 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

类装饰器:

>>> 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 类,并能认识到什么时候元类才是真正适合使用的工具。****