翻译-Python2.2中的新特性

概述

最近在看Python文档,有些文档没有中文版本,然后就想着自己抽时间翻译一下自己感兴趣的章节吧,翻译一趟下来后发现自己对Python的了解又多了一点,还能了解下大神们的设计思路,以后争取多翻译点文档。自己的翻译功底很烂,希望不要误导到别人。

本次翻译的内容总结:

  1. 旧式类和新式类的介绍
  2. 描述器
  3. 多重继承之菱形继承
  4. 属性访问
  5. 迭代器

原文链接

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这个新式类:

1
2
3
4
class C(object):
def __init__ (self):
...
...

这意味在Python 2.2中没有继承任何基类的类都是旧式类。(实际上你可以通过设置一个模块级别的变量__metaclass__–详见PEP 253–来改变这种情况,但是直接继承object更方便)

内建类型的类型对象(type objects)同样可以被当做内建函数使用,命名使用了一种聪明的技巧。Python 中一般都有诸如int(), float(), str()这些内建函数。 在2.2中,它们不再是函数了,而是类型对象(type objects)会在被调用时像工厂函数一样工作。

1
2
3
4
>>> int
<type 'int'>
>>> int('123')
123

为了完善类型集合,新的类型对象如dict()以及file()已经加入到新版本中。下面展示了一个有意思的例子,增加一个lock()方法到文件对象(file object):

1
2
3
4
class LockableFile(file):
def lock (self, operation, length=0, start=0, whence=0):
import fcntl
return fcntl.lockf(self.fileno(), operation, length, start, whence)

已过时的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的实际执行步骤是:

1
2
descriptor = obj.__class__.x
descriptor.__get__(obj)

相对于方法,descriptor.__get__()返回一个可调用的临时对象,同时将实例以及将要调用的方法打包起来。这也是为善么静态方法和类方法的工作机制;它们有专门的描述器仅仅打包方法/类方法。简单来说,静态方法不会传递实例,因此和常规的函数类似。类方法传递类对象,但是并传递对象本身(实例)。静态方法和类方法定义如下:

1
2
3
4
5
6
7
8
class C(object):
def f(arg1, arg2):
...
f = staticmethod(f)
def g(cls, arg1, arg2):
...
g = classmethod(g)

staticmethod(函数接受函数f()并返回打包后的描述器,这样它就可以保存在类对象中。你也许期望存在着特殊的创建诸如方法(def static f, def static f(),或者其他类似的)语法,但是目前并没有定义这些语法(注: Python 2.7后使用装饰器了),这个留给以后的Python 版本解决。

一些像slots和properties的新特性同样通过新式类的描述器实现,而且编写一个完成一些新颖事情的描述器类并不是很困难。比如,可以写一个描述器类使得编写一个方法的埃菲尔式(Eiffel-style)的先决条件和后置条件成为可能。使用这个特征的类定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from eiffel import eiffelmethod
class C(object):
def f(self, arg1, arg2):
# The actual function
...
def pre_f(self):
# Check preconditions
...
def post_f(self):
# Check postconditions
...
f = eiffelmethod(f, pre_f, post_f)

注意到用户在使用新的eiffelmethod()时并不需要了解描述器的相关知识。这也是我认为新的特性不会增加这门语言的复杂性的原因。为了写出eiffelmethod()或者ZODB或者其它类似的方法的少量程序员还是需要对它有了解,但是大部分的用户仅仅只是在通过库进行开发,并不需要注意实现细节。

Multiple Inheritance: The Diamond Rule

通过更改名称解析的规则,多重继承也变得更加又有。参考如下的类的集合(图片取自PEP 253):

1
2
3
4
5
6
7
8
9
10
11
12
13
class A:
^ ^ def save(self): ...
/ \
/ \
/ \
/ \
class B class C:
^ ^ def save(self): ...
\ /
\ /
\ /
\ /
class D

经典类的查找规则简单但是不够灵活;按照深度优先、从左到右的方式寻找基类。当调用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()方法可以写成这样:

1
2
3
4
5
6
class D (B,C):
def save (self):
# Call superclass .save()
super(D, self).save()
# Save D's private information here
...
  • 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属性,该属性通过计算得到,同时设置时也需要进行计算,可以写成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class C(object):
def get_size (self):
result = ... computation ...
return result
def set_size (self, size):
... compute something based on the size
and set internal state appropriately ...
# Define a property. The 'delete this attribute'
# method is defined as None, so the attribute
# can't be deleted.
size = property(get_size, set_size,
None,
"Storage size of this instance")

相比于编写一对当属性为size时采取特殊处理方法,其它属性从__dict__中检索的__getattr()__/__setattr__()方法,这个显然更清晰简单。对size的访问入口将是唯一的,这样会提高调用函数的性能,因为涉及到其它属性都是正常的速度。

最后,通过新的类属性__slots___可以限制对象可用的属性集合。Python 对象具有很强的动态性,在任何时间都可以通过obj.new_attr=1的形式为实例增加一个新的属性。新式类中你可以通过给类属性__slots___定义一个名称集合来限制其合法属性。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> class C(object):
... __slots__ = ('template', 'name')
...
>>> obj = C()
>>> print obj.template
None
>>> obj.template = 'Test'
>>> print obj.template
Test
>>> obj.newattr = None
Traceback (most recent call last):
File "<stdin>", line 1, in ?
AttributeError: 'C' object has no attribute 'newattr'

注意当你获取一个不在__slots__中的属性时会得到AttributeError异常。

PEP 234: Iterators

2.2中的另一个重大更新是C和Python层面的迭代器接口。可以定义对象在被调用时的遍历方式。

2.2之前,使得语句for item in obj工作的方式是定义如下的__getitem()__方法:

1
2
def __getitem__(self, index):
return <next item>

__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异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> L = [1,2,3]
>>> i = iter(L)
>>> print i
<iterator object at 0x8116870>
>>> i.next()
1
>>> i.next()
2
>>> i.next()
3
>>> i.next()
Traceback (most recent call last):
File "<stdin>", line 1, in ?
StopIteration
>>>

在2.2中,Python的for语句不在需要一个序列;而是需要一个在调用iter()时能返回迭代器的变量。为了保持向后兼容,没有实现__iter()___或者tp_iter位的序列会自动生成迭代器,因此for i in [1,2,3]仍正常工作。现在Python解释器遍历一个序列时都会用到迭代器协议。这意味着如下代码都是正确的:

1
2
3
4
5
>>> L = [1,2,3]
>>> i = iter(L)
>>> a,b,c = i
>>> a,b,c
(1, 2, 3)

一些Python的基本类型已经支持了迭代器。在词典(dict)上调用iter()将会返回一个遍历它的键的迭代器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> m = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
... 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
>>> for key in m: print key, m[key]
...
Mar 3
Feb 2
Aug 8
Sep 9
May 5
Jun 6
Jul 7
Jan 1
Apr 4
Nov 11
Dec 12
Oct 10

这只是默认的行为。你还可以通过调用iterkeys()itervalues()iteritems()来迭代键、值、或者键值对。在一个小的相关变化中,in操作现在也可以用于词典,因此key in dict现在等价于dict.has_key(key)

文件同样提供迭代器,它一直调用readline()的方法直到文件末尾。这意味着你可以通过如下方法读取文件每一行:

1
2
3
for line in file:
# do something for each line
...

注意:在迭代器中只能往前走,往回走、重置迭代器或者拷贝迭代器都是不行的。虽然一个迭代器对象能够提供额外的这些能力,但是迭代器协议只需要一个next()方法。