18. 元类与扩展类(也称“杀人笑话”(The Killer Joke))

Python元类

原文by Guido van Rossum 2009-4-24 翻译by kant


在最初的 Python 实现中,类虽然是第一类对象(first class objects),但创建类对象的过程却是不受干涉的。当我们定义一个类:


    ''class ClassName(BaseClass...):
    '' ...method definitions...

这个类的代码块会在它所在的环境中执行。类名、父类组成的元组,以及当前环境会被传递给类创建函数以创建一个类对象。这个创建过程的细节是对用户隐藏的。

第一个提出问题的人是 Don Beaudry,他认为,高阶用户可能需要控制类对象的创建过程——如果类只是一种特殊对象,那么,用户为什么不能通过自定义的方式来创建类对象呢?因此,他建议对代码解析器做一个小小的调整,从而允许用户通过 C语言扩展模块来创建类对象。

这个 1995年引入的小调整,一直被大家称为 “Don Beaudry 钩子”(Don Beaudry hook)或 “Don Beaudry 神技”(Don Beaudry hack),而这个词的另一个意思则是一个国际戏剧团队的节目单(译注:即 Python 名字来源的 Monty Python's Flying Circus)。

后来,Jim Fulton 把这个调整代码通用化了,并在之后一直是 Python 源代码的一部分。直到 2.2 版本中,随着新式类的引入,元类(metaclass)作为标准机制替代了它们。


Don Beaudry 钩子的基本思路是,如果用户可以在创建类对象的过程中置入自己定义的函数,就可以控制类对象的创建过程。即把创建类对象所需的参数——类名、父类元组、当前环境字典传递给另一个,由用户自己提供的类对象构造函数。

而唯一的问题是,我并不想修改用户自定义类的相关句法。

因此,这个钩子让用户提供一个 C语言写的可调用的类型对象(type object),当以这个类型对象的实例作为父类创建类对象时,会调用这个类型对象,而不是按照默认方式进行创建。从而,类的创建过程就完全取决于用户自己提供的这个类型对象了。

现在的 Python 用户可能并不觉得这个思路有什么好大惊小怪的。不过在当时,Python 中的类型对象是不可调用的——比如说,int 实际上并不是一个内置类型,而是一个内置函数,只不过返回的是整型对象的实例而已。用户自定义类倒是可以调用,但其实只是 CALL 函数中的特例。


正是 Don Beaudry 在我大脑中植入的观念,最终导致了元类的引入、新式类的诞生,以及经典类被抛弃的命运。

其实,最初使用这个钩子的只有 Don Beaudry 自己写的一个模块,叫 MESS。而在 1996年末,Jim Fulton 写了一个很受欢迎的第三方包,Extension Classes,也用了这个钩子——不过,随着 Python 2.2 的发布,大家也不再需要这个模块了。

在 Python 1.5 中,用户使用 Don Beaudry 时已经不用另外写一个 C语言扩展了。类的创建过程不仅会检测父类是否存在可调用的类型对象,也会同时检测父类是否存在 class 属性,如果存在,就会直接调用。当时,我为这个新特性写了一篇文章,使很多 Python 用户第一次了解到元类的概念。因为这个概念最初的来源(Don Beaudry hook)让人联想到一个号称能“笑死人”的节目单,大家很快也把我的文章叫做“杀人笑话”。

或许,Don Beaudry 钩子影响最久远的贡献就是类创建函数的 API,这个 API 之后又由元类的实现机制继承了。调用类创建函数时,需要提供三个参数:一个字符串代表类名,一个元组包含所有的父类(可以为空或只有一个父类),以及一个字典,保存这定义这个类的代码块所在作用域的环境变量。函数返回值被赋值给一个以类名作为名称的变量。

开始时,这个 API 其实是创建类的内部 API,因为 Don Beaudry 钩子也用到了一样的调用方式,因此把它改成了公共 API。关于这个 API,最关键的问题就是,定义类的代码块中的语句(比如定义方法等)是先于类创建函数执行的,因此,元类的作用就受到了限制,无法控制类的内部命名空间的初始内容。

当然,在 Python 3 中,这个问题得到了解决,我们修改了一些句法,使元类可以提供一个映射对象,作为类定义语句的执行空间。

在下一篇中,我将为大家介绍元类是如何导致新式类的产生(以及 Python 3 对经典类的抛弃)的。"


发表评论

评论列表,共 0 条评论