• 玩转Python中的元编程
  • 发布于 2个月前
  • 225 热度
    0 评论
术语“元编程”指的是程序具有编写或操纵其自身作为它们资料的潜力。Python支持称为元类的类的元编程。
元类是一个深奥的面向对象编程(OOP)概念,隐藏在几乎所有的Python代码之后。无论你是否意识到它的存在,你都一直在使用它们。大多数情况下,你并不需要了解它。而且大多数Python程序员也很少用到,但是某些情况下你就不得不考虑使用元类。
当你有需要时,Python提供了一种不是所有面向对象语言都支持的功能:你可以深入了解其内部并自定义元类。使用定制元类经常会存在争议,正如Python大咖,创作了Python之禅的蒂姆·彼得斯所言:
“元类比99%的用户所忧虑的东西具有更深的魔法。如果你犹豫考虑是否需要它们,那么实质上你不会需要它们(实际需要它们的人确信他们确实需要,并且不需要进行任何解释)。“    —— 蒂姆·彼得斯
众多Pythonistas(即Python发烧友所熟知的Python大咖)认为你永远不应该使用自定义元类。这样说可能会有点极端,但大部分情况下自定义元类并不是必需的。如果一个问题不是很明显是否需要它们,那么如果以一种更简单的方式解决问题,代码可能会更干净,更具有可读性。
尽管如此,理解Python元类还是很有必要,因为它可以更好地理解Python类的内部实现。你永远不知道:你可能有一天会发现自己处于这样一种情况,即你确切明白自定义元类就是你想要的。

旧式类VS新式类
在Python范畴,一个类可以是两种类型之一。官方术语并没有对此进行确认,所以它们被非正式地称为旧式类和新式类。
旧式类
对于旧式类,类(class)和类型(type)并不完全相同。一个旧式类的实例总是继承自一个名为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'>

新式类
新式类统一了类(class)和类型(type)的概念。如果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
类型(Type)和类(Class)
在Python 3中,所有类都是新式类。因此,Python 3可以交换一个引用对象的类型和类。
注意:在Python 2中,默认所有类都是旧式类。在Python 2.2之前,根本不支持新式类。从Python 2.2开始,可以创建新式类,但必须明确声明它为新式类。
请记住,在Python中,一切都是对象。类也是对象。所以一个类(class)必须有一个类型(type)。那么类的类型是什么呢?
考虑下面的代码:
>>> 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'>

type是一个元类,任何类都是它的实例。就像一个普通的对象是一个类的实例一样,Python中的任何新式类以及Python 3中的任何类都是type元类的一个实例。
综上所述:
1.x是类Foo的一个实例。
2.Foo是type元类的一个实例。
3.type也是type元类的一个实例,所以它是它自己的一个实例。

动态定义类
内置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(<name>, <bases>, <dct>)调用type():
1.<name>指定类名称,将成为该类的__name__属性。
2.<bases>指定继承类的基类元组,将成为该类的__bases__属性。
3.<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,然后在命名空间字典中通过函数名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
自定义元类
重新思考一下先前的这个例子:
>>> 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时自定义实例化行为。如果你要遵循上面的模式,则需要再次定义一个自定义方法,并将其指定为类Foo的实例的__new__()方法。Foo是type元类的一个实例,所以代码如下所示:
# 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的元类,然后对其进行修改。
第一步是定义派生自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
真的是必要的吗?
就像上面的类工厂的例子一样简单,它是metaclasses如何工作的本质。它们允许定制类的实例化。
尽管如此,仅仅为了赋予每个新创建的类的自定义属性attr,确实有点小题大做。你真的需要一个metaclass来实现吗?
在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
 结论
正如蒂姆·彼得斯建议的,元类可以很容易地作为一种“寻找解决问题的方案”,通常不需要创建自定义元类。如果手头上的问题能够以更简单的方式解决,那或许就应该采用。尽管如此,了解元类有助于理解Python的类,并能够识别元类是否是工作中真正适合使用的工具。
用户评论