The History of Python

2010年8月25日星期三

Why Python's Integer Division Floors

英文原文链接: http://python-history.blogspot.com/2010/08/why-pythons-integer-division-floors.html

原文作者: Guido van Rossum

Why Python's Integer Division Floors
为何Python整除运算采用向下取整的规则

今天(又)有人问我,为什么Python中的整除(integer division)返回值向下取整(floor)而不是像C语言中那样向0取整。

在正整数范围内,两者并无实质差别,例如:

>>> 5//2
2

但是当操作数之一为负时,结果是向下取整的,也就是远离0(接近负无穷方向):


>>> -5//2
-3
>>> 5//-2
-3

或许部分人不太适应,数学上有一个较好的解释为何这样做。整除运算(//)和与之密切相关的取模运算(%)满足如下优美的数学关系式(所有变量均为整数):

a/b = q 余数为 r

b * q + r = a 而且 0 <= r < b (假设a和b都>=0)

如果希望将这一关系扩展到a为负(b仍为正)的情况,有两个选择:一是q向0取整,r取负值,这时约束关系变为 0 <= abs(r) < b,另一种选择是q向下(负无穷方向)取整,约束关系不变,依然是 0 <= r < b。


在数学的数论中,数学家总是倾向于第二种选择(参见如下Wikipedia链接)。在Python语言中我也做了同样选择,因为在某些取模操作应用中a取什么符号并不重要。例如从POSIX时间戳(从1970年初开始的秒数)得到其对应当天的时间。因为一天有24*3600 = 86400秒,这一操作就是简单的t % 86400。但是当表达1970年之前的时间,这时是一个负数,向0取整规则得到的是一个毫无意义的结果!而向下取整规则得到的结果仍然是正确的。

另外一个我能想到的应用是计算机图形学中计算像素的位置。我相信这样的应用还有更多。

顺便说一下,b取负值时,仅需要把符号取反,约束关系变为:

0 >= r > b

那么,现在的问题变成,C为啥不采取(Python)这样的选择呢?可能是设计C时硬件不适合这样做,所谓硬件不适合这样做是说指那些最老式的硬件把负数表示为“符号+大小”而不是像现在的硬件用二进制补码表示(至少对整数是用二进制补码)。我的第一台计算机是一台Control Data大型机,它用1的补码来表示整数和浮点数。60个1的序列表示负0!

Tim Peters对Python的浮点数部分洞若观火,对于我想把这一规则推广到浮点数取模运算有些担心。可能他是对的,因为向负无穷取整的规则有可能导致当x是绝对值特别小的负数时x%1.0会丢失精度。但是这还不足以让我对整数取模,也就是//进行修改。

附言:注意我用了//而不是/,这是一个Python 3 语法,而且在Python 2 中也是有效的,它强调了使用者是要进行整除操作。Python 2 中的 / 有可能产生歧义,因为对两个操作数都是整数时或者一个整数一个浮点数或者两个都是浮点数时,返回的结果类型不同。当然,这是另外的故事,详情参见PEP238

2010年7月6日星期二

From List Comprehensions to Generator Expressions

英文原文链接: http://python-history.blogspot.com/2010/06/from-list-comprehensions-to-generator.html

原文作者: Guido van Rossum

From List Comprehensions to Generator Expressions

从 List Comprehension 到 Generator Expression

List comprehension 在 Python 2.0版本添加进来。这一特性始于Greg Ewing的一套补丁,Skip Montanaro 和 Thomas Wouters参与贡献。(如果我没记错,Tim Peters也很提倡这个想法。)本质上,可以看做众所周知的数学家采用的集合符号的Pythonic化解释。例如,通常认为如下

{x | x > 10}


代表所有满足x > 10的x组成的集合。数学里这种形式隐含了读者可接受的全集(例如根据上下文,可能是所有实数或者所有整数)。在Python中没有全集的概念,在 Python 2.0 时连集合的概念也没有。(Sets是一个有趣的故事,我将来会在另一篇博文讨论。)

基于此以及其它方面考虑,Python中采用如下语法形式来表示:

[f(x) for x in S if P(x)]


这条语句产生一个list(列表),包含的值来自 sequence (序列) S,满足 predicate (判定) P,且被function(函数) f map (映射)。if-从句是可选项,且可以存在多个for-从句(每个for-从句可以有自己可选的if-从句)来表示嵌套循环(多for-从句会把多维元素映射到一维列表中,这一需求比较少见,因此实用中不常用)。

List comprehension 提供了内置函数map() 和 filter() 的替代。 map(f, S) 等价于 [f(x) for x in S],filter(P, S) 等价于 [x for x in S if P(x)]。或许有人认为 map() 和 filter() 的语法形式更紧凑,所以 list comprehension 没有多少值得推荐的。然而,如果考察一个更加实际的例子,观点或许就会改变了。假设我们想对一个list中的每个元素增加1,生产一个新的list。list comprehension 的写法是 [x+1 for x in S] 。map() 的写法是 map(lambda x: x+1, S)。这儿的"lambda x: x+1" 部分是Python语法中用于内嵌的匿名函数。

两种形式(list comprehension和map()/reduce())孰优孰劣引起了争论,有人认为争论的关键在于 Python 的 lambda 语法过于繁琐,如果匿名函数能有更简洁的表示形式,那么map()就更有吸引力了。我不同意,我发觉 list comprehension 形式比函数式语法更易读,尤其是当映射函数变得复杂时。另外 list comprehension 比 map和lambda 执行速度更快。这是因为调用一个 lambda 函数就创建了一个新的堆栈结构(stack frame),而 list comprehension 中的表达式无需创建新的堆栈结构。

在list comprehension获得成功,在发明generator(关于generator,将来会在另外一篇展开)之后,Python 2.4 增加了一种近似的语法用以表示结果构成的序列(sequence),但是并不将它具体化(concrete)为一个实际的list。新特征称作 "generator expression"。例如:


sum(x**2 for x in range(1, 11))


这条语句调用内置函数 sum(),参数为一个generator expression, 它 yield 从1到10(包括10)的平方。 sum() 函数把参数中的值加和起来,得到答案385。该语句相对于 sum([x**2 for x in range(1, 11)]) 的优势应当是明显的。后者生成了一个包含所有平方数的list,然后再遍历一次,最后(得到结果后)丢弃该list。对于数量较大的数据,前者在内存方面的节省是一个重要考虑因素。

我还应该提到 list comprehension 和 generator expression 微妙的区别。例如,在Python 2,如下是一个有效的 list comprehension:


[x**2 for x in 1, 2, 3]


然而,这是一个无效的 generator expression:


(x**2 for x in 1, 2, 3)


我们可以通过给"1, 2, 3"部分添加括号来修复它:


(x**2 for x in (1, 2, 3))


在Python 3,你甚至对list comprehension也必须使用括号了:


[x**2 for x in (1, 2, 3)]


然而,对于"常规的"或者"显式的"for-循环,你仍然可以省略括号:


for x in 1, 2, 3: print(x**2)


为何有这种区别,而且为何在Python 3 对 list comprehension 变得更严格了?影响设计包括反向兼容,避免歧义,注重等效,和语言的进化等因素。最初,Python(还没有版本号的时候:-)只有明确的for-循环形式。在'in'之后的部分不会带来歧义,因为它总是最后伴随着一个冒号。我清楚你要做的是对一些已知数值进行循环,因此,你不需要因为必须增加括号而烦恼。写到这里,又让我想起来在Algol-60,你可以这样写:


for i := 1, 2, 3 do Statement


Algol-60 还额外支持利用step-until从句决定表达式的步长,如下:


for i := 1 step 1 until 10, 12 step 2 until 50, 55 step 5 until 100 do Statement


(追忆往事,如果当初Python的foo-循环也能这样支持对多个序列的遍历,也挺酷的,哎。。。)

当我们在Python 2.0 中增加 list comprehension 时,原来的规则依然有效:序列表达式只可能被伴随的右中括号 ']' 或者 'for' 关键词或者 'if' 关键词结束。而且这是好事。

但是,到了 Python 2.4 增加 generator expression 时,我们遇到了歧义性方面的问题: 语法上看一个 generator expression 的括号部分并不是它语法上必须的部分。例如下面例子:


sum(x**2 for x in range(10))


外括号是属于被调用的函数sum()的一部分,里面的 "裸" generator expression 作为第一个参数。因此理论上,如下语句可以有两种解释:


sum(x**2 for x in a, b)


可以有意解释为这样:


sum(x**2 for x in (a, b))


也可以解释为:


sum((x**2 for x in a), b)


(如果我没记错)犹豫了一阵子之后,我决定这种情况不应该猜测,而是 generator comprehension 的 'in' 关键词之后必须是单个表达式(当然,它是 iterable 的)。但是当时我们也不想破坏已存在于 list comprehension 中的代码,因为它已经广为流行了。

设计 Python 3 时,我们决定 list comprehension:


[f(x) for x in S if P(x)]


完全等价于如下利用内置函数 list() 展开的 generator expression:


list(f(x) for x in S if P(x))


于是我们将稍微更严格的 generator expression 语法也同样适用于 list comprehension。

在 Python 3 我们还做了另外的变动,以增加 list comprehension 和 generator expression 的等效程度。Python 2时,list comprehension 会"泄露" 循环控制变量到外边:


x = 'before'
a = [x for x in 1, 2, 3]
print x # this prints '3', not 'before'


这是最初的 list comprehension 实现造成现象;也是数年间 Python 的"肮脏的小秘密"之一。开始由于这样可以使得 list comprehension 性能"越快越好"而作为折衷保留了,虽然偶然会刺痛人毕竟对新手来说算不上常见缺陷。然而对于 generator expression 我们不会再这样做了。 Generator expression 利用 generator 实现,执行 generator 时需要一个隔离的执行帧(separate execution frame)。由此还使得 generator expression (特别是在遍历比较短的序列时) 比 list comprehension 效率略低。

然而到了 Python 3,我们决心利用和 generator expression 的同样实现策略来修缮这个 list comprehension 的 "肮脏的小秘密"。 于是在 Python 3,上例(当然,最后一句修改为 print(x) :-) 将会打印 'before', 这证明 list comprehension 中的'x'只是暂时遮蔽而不是直接使用 list comprehension 之外的'x'。

在你开始担心 list comprehension 在 Python 3 中变慢之前要说的是:感谢大量的 Python 3 实现方面的努力,list comprehension 和 generator expressions 都比 Python 2 中更快了!(而且两者也不再有速度上的差别。)

更新: 当然,我忘了说 Python 3 还支持 set comprehension 和 dictionary comprehension。这是 list comprehension 思路的自然推广。

Method Resolution Order

Method Resolution Order

英文原文链接: http://python-history.blogspot.com/2010/06/method-resolution-order.html
原文作者: Guido van Rossum

方法解析顺序

在支持多重继承的编程语言中,查找方法具体来自那个类时的基类搜索顺序通常被称为方法解析顺序(Method Resolution Order),简称MRO。(Python中查找其它属性也遵循同一规则。)对于只支持单重继承的语言,MRO十分简单;但是当考虑多重继承的情况时,MRO算法的选择非常微妙。Python先后出现三种不同的MRO:经典方式、Python2.2 新式算法、Python2.3 新式算法(也称作C3)。Python 3中只保留了最后一种,即C3算法。

经典类采用了一种简单MRO机制:查找一个方法时,搜索基类简单的深度优先从左到右。搜索过程中第一个匹配的对象作为返回结果。例如,考虑如下类:


class A:
def save(self): pass

class B(A): pass

class C:
def save(self): pass

class D(B, C): pass


对于类D的实例x,它的经典MRO结果是类D,B,A,C顺序。因此查找方法x.save()将会得到A.save()(而不是C.save())。这个机制对于简单情况工作良好,但是对于更复杂的多重继承关系,暴露出的问题就比较明显了。其中问题之一是关于"菱形继承"时的方法查找顺序。例如:


class A:
def save(self): pass

class B(A): pass

class C(A):
def save(self): pass

class D(B, C): pass


此处类D继承自类B和类C,类B和类C均继承自类A。应用传统的MRO,查找方法时在类中的搜索顺序是D, B, A, C, A。因此,语句x.save()将会如前一样调用A.save()。然而,这可能非你所需!既然B和C都从A继承,别人可以争辩说重新定义的方法C.save()可以被看做是比类A中的方法"更具体"(实际上,有可能C.save()会调用A.save()),所以C.save()才是你应该调用的。如果save方法是用于保持对象的状态,不调用C.save()将使C的状态被丢弃,进而程序出错。

虽然当时的Python代码很少存在这种多重继承代码,"新类"(new-style class)的出现则使之成为常见现象。这是由于所有的新类都继承自object这一基类。因此涉及到新类的多重继承总是会产生前面所述的菱形关系。例如:

class B(object): pass

class C(object):
def __setattr__(self, name, value): pass

class D(B, C): pass


而且,object定义了一些方法(例如__setattr__())可以由子类扩展,这时解析顺序更是至关重要。上面代码所属例子中,方法C.__setattr__应当被应用到类D的实例。

为了解决在Python2.2引入的新类所带来的方法解析顺序问题,我采取的方案是在类定义时就计算出它的MRO,并存储为该类对象的一个属性。官方文档中MRO的计算方法为:深度优先,从左到右遍历基类,这个与经典MRO一致,但是如果任何类在搜索中是重复的,只有最后一个出现的位置被保留,其余会从MRO list中删除。因此我们前面这个例子中,搜索顺序将会是D, B, C, A(经典类采用经典MRO,则会是D, B, A, C, A)。

实际上MRO的计算比文档所说的要更复杂。我发现某些情况下新的MRO算法结果不理想。因此还存在一个特例,用于处理两个基类在两个不同的派生类中顺序不同,而这两个派生类又被另外一个类继承的情况。如下代码所示:


class A(object): pass
class B(object): pass
class X(A, B): pass
class Y(B, A): pass
class Z(X, Y): pass


利用文档中描述的新MRO算法,Z关于这些类的MRO为Z, X, Y, B, A, object。(这儿的object是通用基类。)。然而,我不希望结果中B出现在A之前。因此实际的MRO会交换其顺序,产生Z, X, Y, A, B, object。直观上说,算法尝试保持基类在搜索时过程中首次出现的顺寻。例如,对于类Z,他们基类X应当被首先搜索到,因为在继承的list中排序最靠前。既然X继承自A和B,MRO算法会尝试保持其顺寻。这是在我在Python2.2中实际实现的算法,但是文档只提到了前面的不包括特例处理的算法(我幼稚的认为这点小差别不需明言。)。

然而,就在Python 2.2 引入新类不久,Samuele Pedroni就发现了文档中MRO算法与实际代码中观察到结果不一致的现象。而且,在上述特例之外也可能发生不一致情况。详细讨论的结果认为Python2.2采用的MRO是坏的,Python应当采用C3线性化算法,该算法详情见论文"A Monotonic Superclass Linearization for Dylan"(K. Barrett, et al, presented at OOPSLA'96)。

本质上,Python 2.2 MRO的主要问题在于不能保持单调性(monotonicity)。在一个复杂的多层次继承情况,每个继承关系都决定了一个直接的查找顺序,如果类A继承类B,则MRO明显应当先查找A后查找B。类似,如果类B多重继承类C和类D,则搜索顺序中类B应该在类C之前,且类C应该在类D之前。

在复杂的多层次继承情况,始终能满足这一规则就称为保持了单调性。也就是说,当你决定类A会在类B之前查找到,应当再也不会遇到类B需要在类A之前查找的情况(否则,结果是未定义的,应该拒绝这种情况下的多层次继承)。以前的MRO算法未能做到这一点,新的C3算法则在保证单调性方面发挥了效用。基本上,C3的目的在于让你依据复杂多层次继承中所有的继承关系进行排序,如果所有顺序关系都能满足,则排序结果就满足单调性。否则,无法得到确定的顺序,算法会报错,拒绝运行。

于是,在Python 2.3,摈弃了我手工作坊的 2.2 MRO算法,改为采用经过学术审核检验的C3算法。带来的结果之一就是当多层次继承中存在基类顺序不一致情况时,Python将会拒绝这种类继承。还是参加前面例子,对于类X和类Y就存在顺序上不一致,对于类X,规则判定类A应该在类B之前被检查。然而对于类Y,规则认为类B应该在类A之前被检查。单独情况下,这种不一致是可以接受的,但是如果X和Y共同作为另外一个类(例中定义的类Z)的基类,C3算法就会拒绝这种继承关系。这个也算是对应了Zen of Python的"errors should never pass silently"规则。

2010年7月5日星期一

import antigravity

英文原文链接: http://python-history.blogspot.com/2010/06/import-antigravity.html
原文作者: Guido van Rossum

反重力(antigravity)模块源自一幅XKCD漫画,由Skip Montanaro 添加至Python 3中。进一步的详情可以参考如下链接,这是我所知道最早提及此事的出处: http://sciyoshi.com/blog/2008/dec/30/import-antigravity/


但是反重力(antigravity)模块实际起源于更早时候的Google App Engine!App Engine于2008年4月7号发布,反重力(antigravity)模块在临近发布前才添加进来。在距离发布前几周时间,App Engine大多数代码已经冻结,Google的App Engine项目组认为我们应对添加一个复活节彩蛋,当时征集到许多提案,有的过于复杂,有的难于理解,还有些则存在危险性,最后我们选择了“反重力(antigravity)”模块。App Engine的反重力(antigravity)模块比Python3中的对应实现稍微多了一点变化。它定义了一个fly函数可以随机的做如下两件事情之一:有10%的可能重定向到XKCD的反重力(antigravity)漫画;另外90%的可能则是简单的把漫画中的文字显示在HTML页面中(最后一行有漫画链接地址)。要在App Engine的应用中调用反重力(antigravity)模块,需要如下简单代码:


import antigravity

def main():
antigravity.fly()

if __name__ == '__main__':
main()



更新: Python 3 标准库中的反重力模块还有一个彩蛋中的彩蛋,如果你查看源码会发现它定义了一个实现XKCD 中 GEO 哈希算法(geohashing)的函数.

import this and The Zen of Python

英文原文链接: http://python-history.blogspot.com/2010/06/import-this-and-zen-of-python.html

原文作者: Guido van Rossum

Barry Warsaw 撰写了一篇关于import this和the Zen of Python的有趣博文,我希望更多人关注Python history上由于年代久远而变得晦暗的这一段: http://www.wefearchange.org/2010/06/import-this-and-zen-of-python.html

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方法解析顺序)的介绍留在下一篇。

2010年6月22日星期二

New-style Classes

新类

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

原文作者: Guido van Rossum

[漫长的间歇,本系列blog回归了!我将接着去年的写,我尽量加快速度。]

前面我提到Python中对类(Class)的支持是后来添加的。具体实现符合典型的Python式“抄近路”哲学(cut corners),然而随着Python的发展,类(Class)的实现暴露出各种问题,也成为了Python高级用户的热门话题。

类(Class)实现问题之一是限制了对内置类型(built-in types)的子类化(subclass),例如lists, dictionaries, strings和其它一些对象(object)成为无法子类化的“特殊”类型。对于一门“面向对象(object oriented)”语言来说,这种限制多少有些尴尬。

另一问题是整个类型系统(type system)对于用户自定义类(user defined classes)来说是“错”的。假如你创建两个对象a和b,即使a和b是毫无关联的两个类的实例,语句type(a) == type(b)的值仍为真。无须讳言,对于熟悉C++或者Java语言的程序员来说,由于C++和Java等语言中类(Class)与底层的类型系统(type system)紧密相连,该现象相当诡异。

在Python的2.2版本,我重写了类实现,而且是“正确的实现”。这次重写一个Python主要子系统是目前为止最为野心勃勃的变动,肯定会有人因此说我得了“第二系统综合症(second-system syndrome)”。这次重写,不仅解决了内置类型的子类化这一直接问题,我还增加了对真正的元类(metaclass)的支持,并尝试改善原来多重继承(multiple inheritance)时过于简陋的方法解析顺序(method resolution order)。这项工作的主要受Ira Forman和Scott Danforth撰写的《Putting Metaclasses to Work》影响,书中阐明了元类的概念,及其与Smalltalk中类似概念的区别。

这次重写类实现的一个特点是,新类(new-style class)是作为语言的一个新特性引入,而不是完全替换掉旧的类。实际上为了保持向后兼容,Python 2中缺省生成的类仍然是旧的类。要创建一个新类,你只需子类化一个现存的新类,例如object(object是新类类层次的根)。例如:


class A(object):
statements
...


这次新类的引入取得的成绩斐然。新的元类得到了框架实现者的积极支持,实际上由于例外情况变少也使得讲解类更为容易。向后兼容使得新类进化过程中旧类依然正常工作。最后,虽然旧类终将从语言中移除,用户已习惯了用"class MyClass(object)"来声明一个类,也不错。
--
cut corners可参考前期博文:英文中文译文

second-system syndrome可参考:http://en.wikipedia.org/wiki/Second-system_effect