Python 中的元类编程 将面向对象编程推向新的高度
- UID
- 1066743
|
Python 中的元类编程 将面向对象编程推向新的高度
回顾面向对象编程让我们先用 30 秒钟来回顾一下 OOP 到底是什么。在面向对象编程语言中,可以定义 类,它们的用途是将相关的数据和行为捆绑在一起。这些类可以继承其 父类的部分或全部性质,但也可以定义自己的属性(数据)或方法(行为)。在定义类的过程结束时,类通常充当用来创建 实例(有时也简单地称为 对象)的模板。同一个类的不同实例通常有不同的数据,但“外表”都是一样 — 例如, Employee 对象 bob 和 jane 都有 .salary 和 .room_number ,但两者的房间和薪水都各不相同。
一些 OOP 语言(包括 Python)允许对象是 自省的(也称为 反射)。即,自省对象能够描述自己:实例属于哪个类?类有哪些祖先?对象可以用哪些方法和属性?自省让处理对象的函数或方法根据传递给函数或方法的对象类型来做决定。即使没有自省,函数也常常根据实例数据进行划分,例如,到 jane.room_number 的路线不同于到 bob.room_number 的路线,因为它俩在不同的房间。利用自省,还可以在安全地计算 jane 所获奖金的同时,跳过对 bob 的计算,例如,因为 jane 有 .profit_share 属性,或者因为 bob 是子类 Hourly(Employee) 的实例。
元类编程(metaprogramming)的回答以上概述的基本 OOP 系统功能相当强大。但在上述描述中有一个要素没有受到重视:在Python(以及其它语言)中,类本身就是可以被传递和自省的对象。正如前面所讲到的,既然可以用类作为模板来生成对象,那么用什么作为模板来生成类呢?答案当然是 元类(metaclass)。
Python 一直都有元类。但元类中所涉及的方法在 Python 2.2 中才得以更好地公开在人们面前。Python V2.2明确地不再只使用一个特殊的(通常是隐藏的)元类来创建每个类对象。现在程序员可以创建原始元类 type 的子类,甚至可以用各种元类动态地生成类。当然,仅仅因为 可以在 Python 2.2中操作元类,这并不能说明您可能想这样做的原因。
而且,不需要使用定制元类来操作类的生成。一种不太费脑筋的概念是 类工厂:一种普通的函数,它可以返回在函数体内动态创建的类。用传统的 Python 语法,您可以编写:
清单 1. 老式的 Python 1.5.2 类工厂1
2
3
4
5
6
7
8
9
10
11
12
13
| Python 1.5.2 (#0, Jun 27 1999, 11:23:01) [...]
Copyright 1991-1995 Stichting Mathematisch Centrum, Amsterdam
>>> def class_with_method(func):
... class klass: pass
... setattr(klass, func.__name__, func)
... return klass
...
>>> def say_foo(self): print 'foo'
...
>>> Foo = class_with_method(say_foo)
>>> foo = Foo()
>>> foo.say_foo()
foo
|
工厂函数 class_with_method() 动态地创建一个类,并返回该类,这个类包含传递给该工厂的方法/函数。在返回该类之前,在函数体内操作类自身。 new 模块提供了更简洁的编码方式,但其中的选项与类工厂体内定制代码的选项不同,例如:
清单 2. new 模块中的类工厂1
2
3
4
5
6
| >>> from new import classobj
>>> Foo2 = classobj('Foo2',(Foo,),{'bar':lambda self:'bar'})
>>> Foo2().bar()
'bar'
>>> Foo2().say_foo()
foo
|
在所有这些情形中,没有将类( Foo 和 Foo2 )的行为直接编写为代码,而是用动态参数在运行时调用函数来创建类的行为。这里要强调的一点是,不仅 实例可以动态地创建,而且 类本身也可以动态地创建。
元类:寻求问题的解决方案?元类的魔力是如此之大,以至于 99% 的用户曾有过的顾虑都是不必要的。如果您想知道是否需要它们,则可以不用它们(那些实际需要元类的人们确实清楚自己需要它们,不需要解释原因)。— Python 专家 Tim Peters (类的)方法象普通函数一样可以返回对象。所以从这个意义上讲,类工厂可以是类,就象它们可以是函数一样容易,这是显然的。尤其是 Python 2.2+ 提供了一个称为 type 的特殊类,它正是这样的类工厂。当然,读者会认识到 type() 不象 Python 老版本的内置函数那样“野心勃勃”— 幸运的是,老版本的 type() 函数的行为是由 type 类维护的(换句话说, type(obj) 返回对象 obj 的类型/类)。作为类工厂的新 type 类,其工作方式与函数 new.classobj 一直所具有的方式相同:
清单 3. 作为类工厂元类的 type1
2
3
| >>> X = type('X',(),{'foo':lambda self:'foo'})
>>> X, X().foo()
(<class '__main__.X'>, 'foo')
|
但是因为 type 现在是(元)类,所以可以自由用它来创建子类:
清单 4. 作为类工厂的 type 后代1
2
3
4
5
6
7
8
9
10
11
12
13
| >>> class ChattyType(type):
... def __new__(cls, name, bases, dct):
... print "Allocating memory for class", name
... return type.__new__(cls, name, bases, dct)
... def __init__(cls, name, bases, dct):
... print "Init'ing (configuring) class", name
... super(ChattyType, cls).__init__(name, bases, dct)
...
>>> X = ChattyType('X',(),{'foo':lambda self:'foo'})
Allocating memory for class X
Init'ing (configuring) class X
>>> X, X().foo()
(<class '__main__.X'>, 'foo')
|
富有“魔力”的 .__new__() 和 .__init__() 方法很特殊,但在概念上,对于任何其它类,它们的工作方式都是一样的。 .__init__() 方法使您能配置所创建的对象; .__new__() 方法使您能定制它的分配。当然,后者没有被广泛地使用,但对于每个Python 2.2 new 样式的类(通常通过继承而不是覆盖),都存在该方法。
需要注意 type 后代的一个特性;它常使第一次使用元类的人们“上圈套”。按照惯例,这些方法的第一个参数名为 cls ,而不是 self ,因为这些方法是在 已生成的类上进行操作的,而不是在元类上。事实上,关于这点没什么特别的;所有方法附加在它们的实例上,而且元类的实例是类。非特殊的名称使这更明显:
清单 5. 将类方法附加在所生成的类上1
2
3
4
5
6
7
8
9
| >>> class Printable(type):
... def whoami(cls): print "I am a", cls.__name__
...
>>> Foo = Printable('Foo',(),{})
>>> Foo.whoami()
I am a Foo
>>> Printable.whoami()
Traceback (most recent call last):
TypeError: unbound method whoami() [...]
|
所有这些令人惊讶但又常见的做法以及便于掌握的语法使得元类的使用更容易,但也让新用户感到迷惑。对于其它语法有几个元素。但这些新变体的解析顺序需要点技巧。类可以从其祖先那继承元类 — 请注意,这与将元类 作为祖先 不一样(这是另一处常让人迷惑的地方)。对于老式类,定义一个全局 _metaclass_ 变量可以强制使用定制元类。但大多数时间,最安全的方法是,在希望通过定制元类来创建类时,设置该类的 _metaclass_ 类属性。必须在类定义本身中设置变量,因为 如果稍后(在已经创建类对象之后)设置属性,则不会使用元类。例如:
清单 6. 用类属性设置元类1
2
3
4
5
6
7
8
| >>> class Bar:
... __metaclass__ = Printable
... def foomethod(self): print 'foo'
...
>>> Bar.whoami()
I am a Bar
>>> Bar().foomethod()
foo
|
|
|
|
|
|
|