概述
最近在看Python文档,有些文档没有中文版本,然后就想着自己抽时间翻译一下自己感兴趣的章节吧,翻译一趟下来后发现自己对Python的了解又多了一点,还能了解下大神们的设计思路,以后争取多翻译点文档。自己的翻译功底很烂,希望不要误导到别人。
本次翻译的内容总结:
- 旧式类和新式类的介绍
- 描述器
- 多重继承之菱形继承
- 属性访问
- 迭代器
PEPs 252 and 253: Type and Class Changes
Python中对象和类的模型是Python 2.2 中最大和影响最深远的改变。由于这些改变是向后兼容的,因此你的代码可以不用做任何改动而正常运行,并且这些改变还提供了一些神奇的新能力。在开始介绍本文最长最复杂的一节之前,我先提供这些改变的概览和一些讨论。
很久以前我在网上列举了Python设计中的一些不合理地方。其中一个最显著的缺陷是:你无法为在C中实现的Python类型创建子类。举例来说,你无法创建一个内建类型的子类,也就是说你无法通过子类的方式为它增加一个有用的方法。虽然UserList 模块提供一个支持列表(lists)的全部方法并且可以被继承的类,但是存在一个问题是:有许多的C代码依赖一个标准的Python 列表(list),并不能接受一个UserList实例。
Python 2.2 修复了这个问题,而且顺便增加了一些新的特性,简要描述如下:
- 你可以继承任何的内建类型,比如列表(lists)和整型(integers),子类可以在任何需要原始类型的地方正常工作。
- 你可以定义类的静态方法以及类方法,以及在之前的Python版本中可用的实例方法(instance method)。
- 你可以通过一个新的属性(properties)机制来使用方法调用的方式访问和设置一个实例的属性。许多使用__getattr__() 方法的地方可以使用属性代替,这使得生成的代码更加简单和快速。额外的一个好处是属性现在也可以有文档说明(docstrings)了。
- 通过使用__slot__,你可以对一个实例的合法属性进行限制,这样可以避免拼写错误,也许在后面的Python版本中还可以进行更多的优化。
一些用户表示了自己对这些变化的担忧。虽然这些新特性很整洁而且让他们看到了之前Python版本中没有的一些技巧(tricks),但同时也使得这门语言更加复杂了。部分用户曾经说过他们是因为Python足够简洁而被推荐使用的,新特性让他们感到丢失了Python的简洁性。
个人觉得这些担心都是没必要的。许多的新特性是十分深奥的,你甚至可以在忽略它们的情况下编写你的Python代码。写一个简单的类并不比之前版本困难,因此在实际用到这些特性之前,你并不需要学习或者了解它们。一些之前只能在C语言级别实现的复杂任务现在也可以使用纯Python实现了,对我来说,这是一种更好的改进。
我不会尝试覆盖使得新特性工作的所有细节和微小变化。相反,这节仅仅进行大体介绍。通过“相关链接” 了解关于Python 2.2 新对象(object)模型的更多信息。
Old and New Classes
首先,你应该了解的是Python 2.2中存在着两种类型的类:经典(旧式)类和新式类。旧式类与之前版本的行为一模一样。所有本节中描述的新属性都只针对新式类。这种差异并不打算一直持续下去,也许在Python 3.0中旧式类最终会被抛弃。
通过继承一个存在的新式类可以定义你自己的新式类。Python中大部分的内建类型,比如整型、列表、词典、甚至文件,都已经是新式类了。所有内建类型的基类是一个叫做object的新式类,如果没有合适的内建类继承,你可以直接继承object这个新式类:
|
|
这意味在Python 2.2中没有继承任何基类的类都是旧式类。(实际上你可以通过设置一个模块级别的变量__metaclass__–详见PEP 253–来改变这种情况,但是直接继承object更方便)
内建类型的类型对象(type objects)同样可以被当做内建函数使用,命名使用了一种聪明的技巧。Python 中一般都有诸如int(), float(), str()这些内建函数。 在2.2中,它们不再是函数了,而是类型对象(type objects)会在被调用时像工厂函数一样工作。
|
|
为了完善类型集合,新的类型对象如dict()以及file()已经加入到新版本中。下面展示了一个有意思的例子,增加一个lock()方法到文件对象(file object):
|
|
已过时的posixfile 模块下有一个模拟所有文件对象方法的类也增加了lock()方法,但是这个类不能传递到期望接收内建文件的内部函数中去,但是对于我们的新的LockableFile来说是可以的。
Descriptors
在老版本的Python中没有统一的方法获取对象(object)支持的属性和方法。当然有一些非正式的约定,可以通过定义__members__和__methods__属性记录名字列表,但是很多时候扩展类的人并不会花时间去定义它们。你也可以回头检视对象的__dict__属性,但是当类被继承或者使用了一个随意的__getattr__()钩子时得到的结果仍可能不准确。
新式类中一个重要的想法是:提供一个使用规范化的描述器(descriptors)来描述对象属性的API。描述器对应了一个属性的值,用来说明它是否是一个方法或者是一个变量。描述器API使得静态方法和类方法变成可能,以及具有特异的构造。
属性描述器是存在于类对象内部的对象,同时它们拥有一些自己的属性:
- __name__:属性名字
- __doc__:属性的文档说明
- __get__(object):用来检索对象值的方法
- __set__(object, value):用来设置对象值的方法
- __delete__(object, value):删除对象的值
例如,当你写下obj.x
,Python的实际执行步骤是:
|
|
相对于方法,descriptor.__get__()
返回一个可调用的临时对象,同时将实例以及将要调用的方法打包起来。这也是为善么静态方法和类方法的工作机制;它们有专门的描述器仅仅打包方法/类方法。简单来说,静态方法不会传递实例,因此和常规的函数类似。类方法传递类对象,但是并传递对象本身(实例)。静态方法和类方法定义如下:
|
|
staticmethod(
函数接受函数f()
并返回打包后的描述器,这样它就可以保存在类对象中。你也许期望存在着特殊的创建诸如方法(def static f, def static f()
,或者其他类似的)语法,但是目前并没有定义这些语法(注: Python 2.7后使用装饰器了),这个留给以后的Python 版本解决。
一些像slots和properties的新特性同样通过新式类的描述器实现,而且编写一个完成一些新颖事情的描述器类并不是很困难。比如,可以写一个描述器类使得编写一个方法的埃菲尔式(Eiffel-style)的先决条件和后置条件成为可能。使用这个特征的类定义如下:
|
|
注意到用户在使用新的eiffelmethod()
时并不需要了解描述器的相关知识。这也是我认为新的特性不会增加这门语言的复杂性的原因。为了写出eiffelmethod()
或者ZODB或者其它类似的方法的少量程序员还是需要对它有了解,但是大部分的用户仅仅只是在通过库进行开发,并不需要注意实现细节。
Multiple Inheritance: The Diamond Rule
通过更改名称解析的规则,多重继承也变得更加又有。参考如下的类的集合(图片取自PEP 253):
|
|
经典类的查找规则简单但是不够灵活;按照深度优先、从左到右的方式寻找基类。当调用D.save()
时将搜索类D, B,然后最后在A找到save()
方法并且返回。C.save()
方法不会被搜索到。这种方式并不太好,因为如果C的save()
方法将保存一些针对于C的内部状态,不调用它将会导致这个状态绝不被保存。
新式类使用了一个不同的比较难解释的算法,但是在这种情况下会表现正确。(注意在Python 2.3中稍微改变了一下这个算法,使得在大多数情况下有同样的结果,但是在真正复杂的继承图谱时会产生更加有用的结果)
- 列举所有的基类,使用经典查找规则,如果一个类重复访问时会包含多次。在上面的例子中,访问类的列表是[D, B, A, C, A]。
- 扫描列表获取重复的类。如果存在重复类则只留下其中最后一个,其它的全部去掉。在上面的例子中,类列表变为[D, B, C, A]。
根据这个规则,调用D.save()
时将会返回C.save()
,这也是后面版本的行为。此规则和Common Lisp 所遵循的规则相同。通过新的内建函数super()
可以直接调用父类的方法。最常用的方法是super(class, obj)
,它返回一个绑定父类的对象(而不是实际的类对象)。这种形式用于方法中可以调用父类的方法;比如,D中的save()
方法可以写成这样:
|
|
super()
在使用super(class)
或者super(class1, class2)
这种形式时同样能返回未绑定的父类对象,但是这通常不会被用到。
Attribute Access
大量的复杂的Python类定义__getattr__()
钩子(hooks)来访问属性;通常情况下这么做是为了方便,也使得代码具有更好的可读性(通过自动映射诸如obj.parent
这样的属性获取到类似obj.get_parent
方法)。在Python 2.2中增加了新的方法控制属性获取。
首先,新式类依然支持__getattr__(attr_name)
,与之前无任何不同。与之前一样,如果foo
属性不在obj
的实例词典中时,使用obj.foo
时会调用__getattr__
。
新式类同样支持新方法,__getattribute__(attr_name)
。两种方法的不同之处在于访问任何属性时__getattribute__()
都会被调用,但是老的__getattr__()
只会在foo
属性不存在于实例词典时调用。
然而,Python 2.2中对属性(properties)的支持采用了一种更简单的方式来获得属性(attribute)引用。编写一个__getattr__()
方法非常复杂,因为你不能使用在它内部使用常规的属性访问方法(避免无限递归),而且还会把__dict__
的内容搞乱。当检查诸如__repre__()
或者__coerce__()
等方法时Python最终会调用__getattr__
,因此写的时候一定要把这个记在心里。最后,在访问每个属性的时候都调用一个函数会造成可观的性能损失。
属性 (property)是打包一个属性(attribute)的get、set、delete这三个方法的内建类型,如果有文档说明,也会包含它。举例来说,如果你想定义一个size
属性,该属性通过计算得到,同时设置时也需要进行计算,可以写成:
|
|
相比于编写一对当属性为size
时采取特殊处理方法,其它属性从__dict__
中检索的__getattr()__/__setattr__()
方法,这个显然更清晰简单。对size
的访问入口将是唯一的,这样会提高调用函数的性能,因为涉及到其它属性都是正常的速度。
最后,通过新的类属性__slots___
可以限制对象可用的属性集合。Python 对象具有很强的动态性,在任何时间都可以通过obj.new_attr=1
的形式为实例增加一个新的属性。新式类中你可以通过给类属性__slots___
定义一个名称集合来限制其合法属性。如下:
|
|
注意当你获取一个不在__slots__
中的属性时会得到AttributeError
异常。
PEP 234: Iterators
2.2中的另一个重大更新是C和Python层面的迭代器接口。可以定义对象在被调用时的遍历方式。
2.2之前,使得语句for item in obj
工作的方式是定义如下的__getitem()__
方法:
|
|
__getitem()__
更适合的用法是定义对象的索引操作,这样你就可以写obj[5]
这样的语句获取第6个元素。当你使用这个方法只是为了支持for
循环时就显得有点误导性了。比如想要遍历一些文件型对象(file-like object);index参数就显得没有意义了,因为这个类可能假设会多次调用__getitem__()
方法(每次增加index的值)。换句话说,__getitem__()
并不是意味着使用file[5]
随机访问第六个元素,即使它应该这样。
在Python 2.2中,迭代可以分开实现,这样__getitem()__
方法可以仅用作随机访问。迭代的基本思想很简单。新的内建函数iter(obj)
或者iter(obj, sentinel)
用来获取一个迭代器。iter(obj)
返回obj的迭代器对象,iter(C, sentinel)
返回一个重复调用可执行对象C直到遇到sentinel的迭代器。
Python类中可以定义__iter__()
方法来创建和返回作用于对象的新迭代器;如果迭代器就是对象本身,只需要返回self。一般来说,迭代器通常都是自己的迭代器。C中实现的扩展类型可以实现tp_iter
方法来返回一个迭代器,如果一个扩展类型想要拥有迭代器的行为,可以定义tp_iternext
方法。
那么到目前为止,迭代器到底做了些什么?它们有一个next()
方法,该方法无参数同时返回下一个值。当不存在任何值的时候,调用next()
会抛StopIteration
异常。
|
|
在2.2中,Python的for
语句不在需要一个序列;而是需要一个在调用iter()
时能返回迭代器的变量。为了保持向后兼容,没有实现__iter()___
或者tp_iter
位的序列会自动生成迭代器,因此for i in [1,2,3]
仍正常工作。现在Python解释器遍历一个序列时都会用到迭代器协议。这意味着如下代码都是正确的:
|
|
一些Python的基本类型已经支持了迭代器。在词典(dict)上调用iter()
将会返回一个遍历它的键的迭代器:
|
|
这只是默认的行为。你还可以通过调用iterkeys()
、itervalues()
、iteritems()
来迭代键、值、或者键值对。在一个小的相关变化中,in
操作现在也可以用于词典,因此key in dict
现在等价于dict.has_key(key)
。
文件同样提供迭代器,它一直调用readline()
的方法直到文件末尾。这意味着你可以通过如下方法读取文件每一行:
|
|
注意:在迭代器中只能往前走,往回走、重置迭代器或者拷贝迭代器都是不行的。虽然一个迭代器对象能够提供额外的这些能力,但是迭代器协议只需要一个next()
方法。