David Mertz, Ph.D.
总裁,Gnosis Software, Inc.
2001 年 2 月
Python 程序员最近随着版本 2.0 的发行获得了一个崭新的工具。Python 2.0 是在以前 Python 版本的长处上构建的,同时添加了许多新的便利和能力。本文提供了作者对 Python 最新版本的印象,以及有关如何有效利用它的一些技巧。
于 2000 年 10 月发行的 Python 2.0 引入了许多新的语言特性,并包括一些新的标准模块。负责人 van Rossum 的一个美德 -- 可能就是为他在 Python 社区中赢得“终身慈善统治者 (BDFL)”称号的 -- 是他在 Python 更改方面的守旧性。他主张在 Python 的各个版本之间的更改越少越好,而且要提前几个月甚至几年来考虑和讨论要进行的更改。这有利于 Python 的向前和向后兼容性,也有利于在各个平台和版本上运行 Python 程序的一致性。据说,Python 2.0 在 Python 1.5x 的语言定义方面有了一个非常大的跳跃。幸运的是,Python 2.0 仍然维持了良好的向后兼容性,所做的更改在特征方面通常也具有相当的“Python 性”。
另外,值得说明的是短命的Python 1.6 是于 2000 年 9 月发行的。该发行版有些奇怪 -- 它的存在得自于 Python 核心开发小组的合约职责,该小组在 1.6/2.0 开发的同一时期找到了一个新的组织住所。Python 1.6 中的大部分与 Python 2.0 类似,但如果您要安装新版本,最好安装 Python Python 2.0。
有关更改的详尽摘要,请仔细查看参考资料。本文包含的是我个人对最重要和最有趣的内容的主观评价;您所感兴趣的某些更改可能没有在这里涉及。
列表内涵和 zip()
对我来说,Python 2.0 最令人兴奋的新特性是添加了列表内涵。(任何具有数学背景的好奇的读者都会注意到,这种能力在其它函数语言中有时又称为“ZF 内涵”,它是根据 Zermelo-Frankel 集合论中的内涵公理而命名的。)
大多数读者会注意到在前一段中有个奇怪的措辞:“其它函数语言。”作为 Python 程序员,自从 Python 1.0 以来,您已在使用(混合的)函数语言进行编程。当然,如果您还不习惯使用内置函数 lambda()、map()、reduce() 和 filter(),您也还没使用过这些函数特性。但即使您使用过这些,Python 总是使它易于避免考虑函数范例。
无论在什么情况下,列表内涵都是执行许多 Python 内置函数所执行的操作的一种方法,但使用的方式更为紧密,同时更方便了读取和理解。让我们从一个使用中的简单列表内涵的示例开始:
列表内涵示例
>>> xs = (1,2,3,4,5)
>>> ys = (9,8,7,6,5)
>>> bigmuls = [(x,y) for x in xs for y in ys if x*y > 25]
>>> print bigmuls
[(3,9), (4,9), (4,8), (4,7), (5,9), (5,8), (5,7), (5,6)]
|
在该例中,我们创建了一个元组的列表,其中每个元组元素都是从其它列表中抽取出的,而每个列表元素又都满足某些特性。如果不使用 if 子句,我们只能创建一个排列(通常在它们本身中有用);而如果使用 if 子句,我们可以创建一个对列表进行删节的 filter() 类型。顺便说一下,一个列表内涵中允许存在多个 if 子句。
从根本上说,列表内涵能力中没有什么新内容;在 Python 1.5x 中当然也可以达到相同的效果,但没那么清楚。例如,下面一些更冗长(因此不很清晰)的技术可以做同样的事:
版本 1.5x 中各种技术的比较
>>> # Functional-style spaghetti for list comprehension
>>> filter(lambda (x,y): x*y > 25,
..... map(None, xs*len(ys),
..... reduce(lambda s,t: s+t,
..... map(lambda y: [y]*len(xs), ys))))
[(3, 9), (4, 9), (5, 9), (4, 8), (5, 8), (4, 7), (5, 7), (5, 6)]
>>> # Nested loop procedural style for list comprehension
>>> bigmuls = []
>>> for x in xs:
..... for y in ys:
..... if x*y > 25:
..... bigmuls.append((x,y))
>>> print bigmuls
[(3, 9), (4, 9), (4, 8), (4, 7), (5, 9), (5, 8), (5, 7), (5, 6)]
|
在我给出的示例中,嵌套的程序性循环比函数风格的调用更清晰(读者可能会注意到一种更好的函数方法)。但它们远不及列表内涵风格来得清晰。
通过一些程序员的实践,列表内涵可以代替大多数函数风格内置函数,以及许多嵌套循环的使用。
Python 2.0 中一个新的内置函数在与列表内涵结合时特别有用。可以通过想象拉链的牙齿来认识 zip() 的作用:将两个或多个序列组合成元组的列表(每个元组都具有来自每一调用序列的一个元素)。如果您不希望一个使用完整列表排列的列表内涵,而只是一个利用多个列表的相应元素,这通常是有用的。例如:
zip() 函数
>>> zip(xs,ys)
[(1, 9), (2, 8), (3, 7), (4, 6), (5, 5)]
>>> [(x,y) for (x,y) in zip(xs, ys) if x*y > 20]
[(3, 7), (4, 6), (5, 5)]
|
Unicode 支持
对 Python 2.0 的另一个重要增加部分是 Unicode 支持。如果您需要在程序中使用多国字符集,拥有这一能力绝对必要。当然,如果您象我一样除 ASCII 以外没有任何特定的字符需求,Unicode 支持并不那么重要。幸运的是,Python 2.0 中 Unicode 的实现设计得十分完美,而且不防碍任何其它功能。
Unicode 字符串可以用几种方式表示。对于换码,可以使用序列 "uHHHH",其中 HHHH 是个四位十六进制数。可以使用新的 Unicode 引用语法输入更长一些的字符串:u"string"。如果不在 Python 级别上解析转义码,则它在风格上与组成规则表达式所使用的 r"string" 引用样式非常类似(因为规则表达式使用某些相同的转义码)。当然,要使用 Unicode 引用语法,需要拥有一个文本编辑器,能够在引号之间输入 Unicode 字符。
8 位字符串与 Unicode 字符串之间的转换 -- 以及同样不同 Unicode 编码之间的转换 -- 是通过使用新的 codecs 模块执行的。
函数/方法调用语法
另一个很好的语法增强是对函数调用进行的。现在有可能使用自变量元组和/或关键字自变量字典来直接调用函数。和列表内涵一样,没有添加实质性的新能力,而是使某些常见操作的表达更清楚更简洁。当然,Python 中的方法只是与类实例绑定的一些函数,所以对于函数和方法来说,每件事都一样起作用。
Python 程序员将熟悉前面用于定义函数定义中额外的位置和关键字自变量的语法。例如,可以:
定义额外的位置和关键字自变量
>>> defmyfunc(this, that, *extras, **keywords):
.... print "Required arguments:", this, that
.... print "Extra arguments:",
.... for arg in extras: print arg,
.... print "Dictionary arguments:"
.... for key,val in keywords.items(): print "**", key, "=", val
....
>>> myfunc(1)
Traceback (innermost last):
File "<interactive input>", line 1, in ?
TypeError: not enough arguments; expected 2, got 1
>>> myfunc(1,2)
Required arguments: 1 2
Extra arguments:
Dictionary arguments:
>>> myfunc(1,2,3,4,5)
Required arguments: 1 2
Extra arguments: 3 4 5
Dictionary arguments:
>>> myfunc(1,2,3, spam=17, eggs=32)
Required arguments: 1 2
Extra arguments: 3
Dictionary arguments:
** spam = 17
** eggs = 32
|
Python 2.0 添加了与函数定义所使用的相同的函数调用约定。例如:
函数调用的约定
>>> argdict = {'spam':'tasty', 'eggs':'over easy'}
>>> arglist = [1,2,3,4,5]
>>> myfunc(*arglist, **argdict)
Required arguments: 1 2
Extra arguments: 3 4 5
Dictionary arguments:
** spam = tasty
** eggs = over easy
|
原先在 Python 中总可以达到相同的效果(通过命名的列表传递自变量,可能是在运行时动态创建的)。但新的调用语法比旧的 apply() 函数的用法更方便。
增加的赋值
现在,Python 在赋值方面有了一个快捷方式,它将为 C、Perl、Awk、Java 和各种其它语言的程序员所熟悉。现在有可能将一个运算符固定在等号左侧,来基于其旧值更改所赋的变量的值。例如:
赋值中的新快捷方式
>>> i = 1
>>> i += 1 ; i
2
>>> i *= 3 ; i
6
>>> i /= 2 ; i
3
>>> str = "Spam and eggs"
>>> str += "...and sausage and spam and bacon" ; str
'Spam and eggs...and sausage and spam and bacon'
|
从语义上说,增加的运算符所执行的操作完全等同于以下操作:在普通赋值左侧重复左侧变量,再在它后面跟上相应的运算符和另一个操作数。所以从这一意义上说,它只是一种语法上的粉饰。
但请注意,增加的赋值实际上是对性能方面的改进。我没有亲自衡量过它,但该讨论使人想到,使用增加的赋值可以节省查表和某些对象分配的工作。对数来说,这并不重要;但如果您碰巧使用的是几兆字节的字符串,使用增加的赋值可以加快速度,同时减少内存的使用。
无用信息收集
Python 的内存管理对于大多数普通 Python 程序员来说可能相当神秘。传统上,Python 使用一个引用计数方案来在对象不再能从任何名称访问时删除它们。不过,如果在程序中使用循环引用,引用计数方法从理论上说容易造成内存泄露。例如,以下代码会破坏引用计数:
Python 中的循环引用
>>> classMyClass: pass
....
>>> myobject = MyClass()
>>> myobject.me = myobject
>>> del myobject
|
这时,不可能访问 myobject,但它还未被删除,因为引用计数增加了两次,但只消耗了一次。
虽然这听上去很严重,但大多数程序员永远不会遇到由于类似上述代码而产生的任何实际问题。在大多数情况下,不会一上来就使用循环引用,即使使用它们,大多数情况下,内存泄露也是很轻微的(可以轻易地构造人为的危险情况;例如,向上例中添加 myobject.big='#'*10**6)。
在任何情况下,Python 2.0 都会添加一个编译时选项,用于标记和清扫无用信息收集 (GC)。大多数 Python 2.0 发行版似乎都是使用这一选项编译的;但如果您需要,可以编译关闭无用信息收集选项的自己的 Python 版本。无论在哪种情况下,都仍使用引用计数;这只是类似上面的泄露是否被清除的问题。
在某些平台上,例如嵌入式系统,GC 可能不受欢迎。无用信息收集要占用一些 CPU 周期(不是很多,但有一些)。可能最为重要的是,引用计数是在程序行为中确定的,而无用信息收集则不是。这就是说,永远也不能确切地知道何时无用信息收集将吃掉一些 CPU 周期;因此,使用 GC 版本的 Python 将导致在不同的运行中,同样的程序有不同的行为(在时间安排方面)。
这些问题从理论上来说很令人着迷,但从现在起,大多数程序员应该只是忽略它们。无论您选择的是哪一种 Python 发行版,几乎都可以对您所使用的平台执行正确的操作;除非您确切知道为什么希望启用或禁用 GC,否则建议您不要管它。
打印指令
虽然 van Rossum 以及小组其他成员使用 Python 2.0 进行了一些不错的尝试,但他们也将一个瑕疵引入了 Python。它对某些事物相当有用,但在我看来(也在许多其他 Python 程序员看来),它在原来没有的地方引入了一个全新的(也是不好的)语法特性。不过,大多数程序员怀疑这一缺陷只不过是个计策,是为了使 Python 其余部分的简单性和长处发挥得更加出色。
print 语句执行了文件对象的 .write() 方法不具备的一点魔法(sys.stdout 只是 print 写入的另一个文件对象)。print 语句允许多个可以属于任何 Python 类型的自变量。结尾的逗号方便地允许在 print 语句之间续行,而缺省情况是将每一组写入自己的一行中。总的说来, print 只是从程序到控制台获得一些信息的便捷方法。
许多 Python 程序员都曾希望将同样的 print 巫术用于写入其它文件对象(例如 sys.stderr、常规文件,或各种模块提供的“类似于文件”的对象)。我认为这样做的正确方法是向文件对象添加一个 .print() 方法,然后在那里执行魔术。但 Python 2.0 是通过向 print 语句添加“重定向运算符” >> 来添加这一能力的。例如:
print 语句中的重定向运算符
>>> import sys
>>> print >> sys.stderr, "spam", [1,2,3], 45.2
spam [1, 2, 3] 45.2
|
它能够工作 -- 并添加了一种不错的能力 -- 但它只是将 Python 向某些其它编程语言的“可执行行噪声”感受推进了一小步。
参考资料
关于作者
David Mertz 撰写了许多预言性文章。可以通过 mertz@gnosis.cx 与他联系;Gnosis 网站上详细介绍了他的生活。非常欢迎对过去的、这一篇或将来的专栏文章提出意见和建议。