The History of Python

2010年6月24日星期四

The Inside Story on New-Style Classes

The Inside Story on New-Style Classes

新类内幕

英文原文链接: http://python-history.blogspot.com/2010/06/inside-story-on-new-style-classes.html

原文作者: Guido van Rossum

[提醒,本篇是长文,且多技术细节。]

表面看新类和原来的类实现十分相似,实际上新类还引入了一些新概念:
  • 低级构造方法__new__()
  • descriptors,泛化个性化属性(attribute)存取方式
  • 静态方法与类方法(static methods and class methods)
  • properties (即时计算的attributes)
  • decorators (Python2.4引入)
  • slots
  • 新的MRO(Method Resolution Order, 方法解析顺序)
接下来几个章节我将尝试介绍一下这些概念

低级构造方法和__new__()
往常,类的实例创建后,通过__init__()方法进行初始化。然而有时类的作者希望能控制实例的创建过程-例如,当对象是从存储数据恢复而来时。旧式类(Old-style classes)从未真正提供定制对象创建的机制,虽然有些库模块(例如,"new"模块)提供了某些情况的对象创建非标准方式。

新类引入了新的类方法__new__(),以方便类作者控制类实例创建。类作者通过重载__new__()可以实现一些设计模式,例如(从空闲列表)返回已创建实例的单模式(Singleton Pattern),或者返回其它类(可能是原来类的子类)的实例。__new__还有其它重要用途值得关注,例如,在pickle模块中,在反序列化对象时__new__用于创建实例。这时实例已经生成,而__init__方法尚未调用。

__new__还可用于辅助子类化不可变类型(immutable types)。因为"不可变"这一特性,这类对象无法通过标准的__init__()方法来初始化。于是任何特殊的初始化,需要在对象创建时进行;例如,如果类希望能修改不可变对象包含的值,可在__new__方法对此进行处理,向基类的__new__方法传递修改后的值来实现。

Descriptors

Descriptors 是绑定方法(bound methods)这一概念的泛化,绑定方法对于经典类(classic classes)实现至关重要。在经典类情况,当要查询的实例属性(instance attribute)在实例字典(instance dictionary)中未找到时,就继续在类字典中(class dictionary)查找,然后是递归遍历基类的类字典。当属性是在类字典(class dictionary)中查找到时(而不是在实例字典中)。解释器会检查查找到的对象是否是一个Python函数对象(function object)。如果是,则返回值不是该查找到的查找到的对象,而是将该对象的包装对象(wrapper object)作为一个currying function返回。当包装对象被调用时,它会把实例插入到参数列表最前面,然后调用原来的函数对象。

例如,类C有实例x。假设要做方法调用x.f(0)。这个操作会查找x的属性名"f",然后将0作为参数进行调用。如果"f"是类方法,在查找该属性时会返回一个包装函数(wrapper function),可用如下Python伪代码近似描述:


def bound_f(arg):
return f(x, arg)


当传递参数0来调用包装函数时,它会传递两个参数调用"f":x和0。这是类方法得到"self"参数的基本机制。

另外一种调用函数对象(function object)f(无包装)的方式是通过调用类C的属性名"f"。这时返回的不是包装后的对象而是函数f本身。换句话说,x.f(0)等价于C.f(x, 0)。这是Python中的一种相当基本的等价方式。

对经典类而言,如果查找到的属性是其它类型对象(非函数类型),则不创建包装对象,而是不做变化直接将该类属性返回。这使得类属性可以作为实例变量"缺省"值。例如在上例中,如果类C有一个名为"a"的属性,它的值是1,且x的实例字典中没有"a",那么x.a等于1。对x.a赋值则会在x的实例字典创建名为"a"的键,它的值将会遮蔽(shadow)同名类属性(因为属性查找顺序)。当x.a被删除后,被遮蔽的值(1)又能访问到了。

不幸的是,一些Python开发人员发现这种机制存在缺陷。缺陷之一是这种机制下无法创建"混合"类,也就是部分方法用Python实现,部分方法用C实现的类,因为这种机制下只有Python函数才会被包装,使得调用时能访问实例,而且这一行为是硬编码在语言中。而且也没有明显的方式来定义其它类型函数,例如C++和Java程序员熟悉的静态成员函数。

为了解决这一问题,Python 2.2对上述包装行为的进行了直观的泛化。不再用硬编码方式指定只对Python函数对象进行包装,而对其它对象不做包装。包装相关行为完全留给属性搜索到的对象来处理(上例中是函数f)。如果查找属性时的查找结果是有一个名为__get__特殊方法的对象,则该对象被认为是一个"descriptor"对象,这时__get__方法被调用,由它的返回值都作为属性查找结果。如果该对象没有__get__方法,则不作变化直接返回查找到的对象。要得到原有行为(对函数对象进行包装)而且不需要在实例属性查找代码中对函数对象特殊处理,现在拥有一个__get__方法的函数对象将如同以前一样返回包装对象。而且,用户还可以定义含有名为__get__方法的另外一个类,当这种类的实例在原来的类字典中被属性查找过程找到时,可以按需灵活进行包装。

在对属性查找这一概念进行泛化后,同样思路也有必要推广到属性的赋值与删除操作。因此,对赋值操作x.a=1或者删除操作 del x.a也有相似的处理机制。这时,如果属性"a"是在实例的类字典(而不是实例字典)中,检查类字典的中该对象是否含有名为__set__和__delete__的特殊方法(__del__已另有它用)。这样,通过实现这些函数,一个descriptor对象可以完全控制一个属性的get、set和delete含义。要强调的是,这些定制操作仅是对类字典中descriptor实例而言的,对象实例字典中则无效。

staticmethod, classmethod, and property

Python2.2利用这一新的descriptors机制增加了三个预定义类:类方法(classmethod),静态方法(staticmethod)和property,类方法和静态方法只是简单包装函数对象,通过__get__方法的具体实现,来返回不同种类的封装器(wrappers)。例如,静态方法在函数调用时封装器对参数列表不做任何修改。类方法在函数调用时将封装器将实例的类对象增加为第一个参数,而不是增加实例本身。这两种类型的方法都可以通过实例或者类来调用,且在不同调用者情况下调用时的参数相同。

property类封装器将取值和赋值这一对方法转换为一个"属性"。例如,如果你有如下一个类,


class C(object):
def set_x(self, value):
self.__x = value
def get_x(self):
return self.__x


property封装器可用来产生一个属性"x",当存取x时隐含方式调用了get_x和set_x方法。

一开始引入类方法、静态方法和property这些descriptors时,并没有同时引入相应的新语法。当时看起来同时引入比较重要的新特性以及相应的语法引起的争议过大(新语法的讨论总是引起激烈争论)。因此使用这些特性时,先依照常规定义类和方法,然后用额外语句来"包装"方法,例如:


class C:
def foo(cls, arg):
...
foo = classmethod(foo)
def bar(arg):
...
bar = staticmethod(bar)


对properties也有类似机制:


class C:
def set_x(self, value):
self.__x = value
def get_x(self):
return self.__x
x = property(get_x, set_x)


Decorators

这种方式有一个不利之处,用户阅读类的代码时,只有读完一个方法的声明,到了结尾部分才知道该方法是不是类方法或者静态方法(或其它用户自定义形式)。终于,Python2.4引入了新的语法,允许以如下方式撰写代码:


class C:
@classmethod
def foo(cls, arg):
...
@staticmethod
def bar(arg):
...


在函数声明前一行构造的@表达式(@expression)称为decorator。(不要和前面的descriptor混淆,实现__get__包装时所讨论到的才是descriptor。)选择这个decorator语法(decorator syntax)(从Java的annotations借鉴而来。)引起了无休止的争论,最终由BDFL宣告(BDFL pronouncement)来确定。(David Beazley写了一篇关于术语BDFL的历史典故,我会另外单独写一篇贴出。)

decorator特性已成为最成功的Python语言特性之一,各种decorator应用超过我当初最乐观的期待。特别是在Web框架中大量应用。鉴于如此成功,在Python2.6中,decorator语法从函数定义进一步扩展到类定义。

Slots

descriptor的引入带来了另外一个新特性,也即类的__slots__属性。例如一个类,定义如下:


class C:
__slots__ = ['x','y']


这儿的__slots__有如下含义。首先,对象的有效属性名被限制为__slots__值的列表。其次,由于属性名范围已经确定,__dict__属性因不再需要而被移除(除非某个基类已经包含了__dict__属性,子类化当前类且不包含__slots__属性可以再次拥有__dict__属性)。这样各属性存储在预先确定顺序的array中。因此,每一个slot属性实际是一个descriptor对象,利用该属性在array中的位置(index)来调用其set/get方法。在具体实现方面,该特性完全由C语言实现,性能优异。

一些人错误的认为__slots__的目的在于(通过限制属性名)增加代码的安全性。实际上,我的终极目标聚焦在性能方面。__slots__是一个有趣的descriptor应用,我担心对类的改动总是带来性能方面的不利影响。以descriptor而言,为了使之正常工作,对象属性的任何操作都需要首先检查类字典来判定该属性是否实际上是一个数据descriptor(data descriptor)。如果确实是,则由该descriptor负责属性存取,而不是通常情况下直接操作属性字典。这个额外的检查也意味着检查每个实例的字典之前要有一个额外的查找操作。因此,__slots__可以作为一种优化数据属性的方法,作为当有人对新类的性能不满意时的补救。结果证明这种性能的担心并不必要,但是撤除__slots__也为时已晚了。而且,正确使用slots确实能增加性能,尤其是创建大量小对象时有利于降低内存占用。

我把Python MRO(Method Resolution Order方法解析顺序)的介绍留在下一篇。

没有评论: