Python 2.0 的新特性

作者:

A.M. Kuchling 和 Moshe Zadka

简介

Python 的新版本 2.0 于 2000 年 10 月 16 日发布。本文介绍了 2.0 中令人兴奋的新特性,重点介绍了一些其他有用的更改,并指出了少数可能需要重写代码的不兼容更改。

Python 的开发在版本发布之间从未完全停止,并且始终在提交稳定的错误修复和改进。2.0 中包含大量次要修复、一些优化、额外的文档字符串和更好的错误消息;要列出所有这些是不可能的,但它们肯定是重要的。如果您想查看完整列表,请查阅公开可用的 CVS 日志。此进展归功于现在为 PythonLabs 工作的五位开发人员,他们现在每天都有报酬来修复错误,也归功于迁移到 SourceForge 后改进的沟通。

Python 1.6 怎么样了?

Python 1.6 可以被认为是“合同义务”Python 版本。在核心开发团队于 2000 年 5 月离开 CNRI 后,CNRI 要求创建一个 1.6 版本,其中包含 CNRI 执行的所有 Python 工作。因此,Python 1.6 代表了截至 2000 年 5 月的 CVS 树状态,其中最重要的新特性是 Unicode 支持。当然,在 5 月之后开发仍在继续,因此 1.6 树收到了一些修复,以确保它与 Python 2.0 向前兼容。因此,1.6 是 Python 演变的一部分,而不是一个分支。

那么,您应该对 Python 1.6 感兴趣吗?可能不会。1.6final 和 2.0beta1 版本在同一天(2000 年 9 月 5 日)发布,计划在一个月左右完成 Python 2.0。如果您有应用程序需要维护,那么通过迁移到 1.6、修复它们,然后在 2.0 迁移后一个月内再次出现中断,似乎没有什么意义;您最好直接转到 2.0。本文档中描述的大多数真正有趣的功能仅在 2.0 中,因为在 5 月到 9 月之间完成了大量工作。

新的开发流程

Python 2.0 中最重要的变化可能不是代码本身,而是 Python 的开发方式:2000 年 5 月,Python 开发人员开始使用 SourceForge 提供的工具来存储源代码、跟踪错误报告和管理补丁提交队列。要报告 Python 2.0 的错误或提交补丁,请使用 Python 项目页面提供的错误跟踪和补丁管理工具,该页面位于 https://sourceforge.net/projects/python/

现在托管在 SourceForge 上的最重要的服务是 Python CVS 树,这是包含 Python 源代码的版本控制存储库。以前,大约有 7 人左右拥有 CVS 树的写入权限,所有补丁都必须由这个短名单上的一个人检查和签入。显然,这不是很可扩展。通过将 CVS 树移动到 SourceForge,可以向更多人授予写入权限;截至 2000 年 9 月,有 27 人能够签入更改,增加了四倍。这使得大规模更改成为可能,如果它们必须通过一小群核心开发人员进行过滤,则不会尝试这些更改。例如,有一天,Peter Schneider-Kamp 突发奇想,放弃了 K&R C 兼容性,并将 Python 的 C 源代码转换为 ANSI C。在获得 python-dev 邮件列表的批准后,他发起了一连串的签入,持续了大约一周,其他开发人员也加入进来帮忙,工作完成了。如果只有 5 人拥有写入权限,那么这项任务可能会被视为“很好,但不值得花费时间和精力”,并且永远不会完成。

转向使用 SourceForge 的服务导致开发速度显着提高。现在,提交补丁、对其进行评论、由提交者以外的人员修改,并在人员之间来回传递,直到认为该补丁值得签入为止。在一个中心位置跟踪错误,可以将其分配给特定的人员进行修复,并且我们可以计算未解决的错误数量来衡量进度。这并非没有代价:开发人员现在需要处理更多的电子邮件、需要关注更多的邮件列表,并且需要为新环境编写特殊工具。例如,SourceForge 发送的默认补丁和错误通知电子邮件消息完全没有用处,因此 Ka-Ping Yee 编写了一个 HTML 屏幕抓取器,可以发送更有用的消息。

添加代码的便利性导致了一些最初的成长的烦恼,例如代码在准备就绪之前就被签入,或者没有得到开发人员组的明确同意。出现的批准过程与 Apache 组使用的过程有些相似。开发人员可以对补丁投票 +1、+0、-0 或 -1;+1 和 -1 表示接受或拒绝,而 +0 和 -0 表示开发人员对更改大多漠不关心,但略有正面或负面的倾向。与 Apache 模型最显着的区别在于,投票本质上是咨询性的,让拥有终身仁慈独裁者地位的 Guido van Rossum 知道总体意见是什么。他仍然可以忽略投票结果,即使社区不同意他,也可以批准或拒绝更改。

生成实际补丁是添加新功能的最后一步,与早期提出良好设计的任务相比,通常很容易。对新功能的讨论通常会爆发成冗长的邮件列表线程,使得讨论难以跟进,而且没有人可以阅读 python-dev 的每篇文章。因此,已经建立了一个相对正式的流程来编写 Python 增强提案 (PEP),该流程以互联网 RFC 流程为模型。PEP 是描述拟议的新功能的草案文件,并且会不断修订,直到社区达成共识,要么接受该提案,要么拒绝该提案。引用 PEP 1 “PEP 目的和指南”的介绍:

PEP 代表 Python 增强提案。PEP 是一份设计文档,向 Python 社区提供信息,或描述 Python 的新功能。PEP 应提供该功能的简明技术规范和该功能的理由。

我们希望 PEP 成为提出新功能、收集社区对某个问题的意见以及记录 Python 设计决策的主要机制。PEP 作者负责在社区内建立共识并记录异议。

阅读 PEP 1 的其余部分,了解 PEP 编辑流程、样式和格式的详细信息。PEP 保存在 SourceForge 上的 Python CVS 树中,尽管它们不是 Python 2.0 发行版的一部分,并且也可以从 https://peps.pythonlang.cn/ 以 HTML 格式获得。截至 2000 年 9 月,共有 25 个 PEP,从 PEP 201“Lockstep Iteration”到 PEP 225“Elementwise/Objectwise Operators”。

Unicode

Python 2.0 中最大的新特性是一种新的基本数据类型:Unicode 字符串。Unicode 使用 16 位数字来表示字符,而不是 ASCII 使用的 8 位数字,这意味着可以支持 65,536 个不同的字符。

Unicode 支持的最终接口是通过 python-dev 邮件列表中无数次通常是暴风雨般的讨论而得出的,并且主要由 Marc-André Lemburg 基于 Fredrik Lundh 的 Unicode 字符串类型实现来实现。该接口的详细说明编写为 PEP 100 “Python Unicode 集成”。本文将仅介绍有关 Unicode 接口的最重要几点。

在 Python 源代码中,Unicode 字符串写为 u"string"。可以使用新的转义序列 \uHHHH 写入任意 Unicode 字符,其中 HHHH 是从 0000 到 FFFF 的 4 位十六进制数。也可以使用现有的 \xHH 转义序列,并且可以将八进制转义用于 U+01FF 之前的字符,该字符由 \777 表示。

Unicode 字符串与常规字符串一样,是一种不可变的序列类型。可以对其进行索引和切片,但不能就地修改。Unicode 字符串有一个 encode( [encoding] ) 方法,该方法返回所需编码的 8 位字符串。编码由字符串命名,例如 'ascii''utf-8''iso-8859-1' 等。定义了一个编解码器 API,用于实现和注册新的编码,然后这些编码可在整个 Python 程序中使用。如果未指定编码,则默认编码通常为 7 位 ASCII,但可以通过在 site.py 的自定义版本中调用 sys.setdefaultencoding(encoding) 函数来更改 Python 安装的编码。

组合 8 位和 Unicode 字符串始终使用默认 ASCII 编码强制转换为 Unicode;'a' + u'bc' 的结果是 u'abc'

已添加新的内置函数,并修改了现有内置函数以支持 Unicode

  • unichr(ch) 返回一个长度为 1 的 Unicode 字符串,其中包含字符 ch

  • ord(u),其中 u 是一个长度为 1 的常规或 Unicode 字符串,返回字符的数字(整数)。

  • unicode(string [, encoding] [, errors] ) 从 8 位字符串创建一个 Unicode 字符串。encoding 是一个指定要使用的编码的字符串。errors 参数指定如何处理当前编码无效的字符;传递 'strict' 值会导致在任何编码错误时引发异常,而 'ignore' 会使错误被静默忽略,'replace' 会在出现任何问题时使用 U+FFFD,即官方的替换字符。

  • exec 语句以及各种内置函数,如 eval()getattr()setattr() 也将接受 Unicode 字符串以及常规字符串。(在修复此问题的过程中,可能会遗漏一些内置函数;如果您发现某个内置函数接受字符串但不接受 Unicode 字符串,请将其报告为错误。)

一个新的模块 unicodedata 提供了对 Unicode 字符属性的接口。例如,unicodedata.category(u'A') 返回 2 个字符的字符串 ‘Lu’,其中 ‘L’ 表示它是一个字母,‘u’ 表示它是大写字母。unicodedata.bidirectional(u'\u0660') 返回 ‘AN’,表示 U+0660 是一个阿拉伯数字。

codecs 模块包含查找现有编码和注册新编码的函数。除非您想实现新的编码,否则您最常会使用 codecs.lookup(encoding) 函数,它返回一个包含 4 个元素的元组:(encode_func, decode_func, stream_reader, stream_writer)

  • encode_func 是一个接受 Unicode 字符串并返回 2 元组 (string, length) 的函数。string 是一个包含 Unicode 字符串的一部分(可能全部)转换为给定编码的 8 位字符串,而 length 告诉您有多少 Unicode 字符串被转换。

  • decode_funcencode_func 相反,它接受 8 位字符串并返回 2 元组 (ustring, length),包括生成的 Unicode 字符串 ustring 和整数 length,表示消耗了多少 8 位字符串。

  • stream_reader 是一个支持从流中解码输入的类。stream_reader(file_obj) 返回一个支持 read()readline()readlines() 方法的对象。这些方法都将从给定的编码进行转换并返回 Unicode 字符串。

  • 类似地,stream_writer 是一个支持将输出编码到流的类。stream_writer(file_obj) 返回一个支持 write()writelines() 方法的对象。这些方法期望 Unicode 字符串,并在输出时将其转换为给定的编码。

例如,以下代码将 Unicode 字符串写入文件,并将其编码为 UTF-8

import codecs

unistr = u'\u0660\u2000ab ...'

(UTF8_encode, UTF8_decode,
 UTF8_streamreader, UTF8_streamwriter) = codecs.lookup('UTF-8')

output = UTF8_streamwriter( open( '/tmp/output', 'wb') )
output.write( unistr )
output.close()

以下代码将从文件中读取 UTF-8 输入

input = UTF8_streamreader( open( '/tmp/output', 'rb') )
print repr(input.read())
input.close()

可通过 re 模块使用 Unicode 感知的正则表达式,该模块有一个由 Secret Labs AB 的 Fredrik Lundh 编写的名为 SRE 的新底层实现。

添加了一个 -U 命令行选项,该选项会使 Python 编译器将所有字符串文字解释为 Unicode 字符串文字。这旨在用于测试和未来证明您的 Python 代码,因为 Python 的未来版本可能会放弃对 8 位字符串的支持,而仅提供 Unicode 字符串。

列表推导式

列表是 Python 中常用的数据类型,许多程序会在某个时刻操作列表。列表上的两个常见操作是循环遍历它们,并挑选出满足特定条件的元素,或将某个函数应用于每个元素。例如,给定一个字符串列表,您可能想要提取所有包含给定子字符串的字符串,或者从每行中删除尾随的空格。

现有的 map()filter() 函数可以用于此目的,但它们需要一个函数作为参数之一。如果有一个现有的内置函数可以直接传递,这很好,但如果没有,您必须创建一个小函数来完成所需的工作,并且如果这个小函数需要其他信息,Python 的作用域规则会使结果变得难看。以前面段落中的第一个示例为例,查找列表中所有包含给定子字符串的字符串。您可以编写以下代码来完成它

# Given the list L, make a list of all strings
# containing the substring S.
sublist = filter( lambda s, substring=S:
                     string.find(s, substring) != -1,
                  L)

由于 Python 的作用域规则,使用默认参数,以便 lambda 表达式创建的匿名函数知道正在搜索哪个子字符串。列表推导式使这个更加简洁

sublist = [ s for s in L if string.find(s, S) != -1 ]

列表推导式的形式如下

[ expression for expr in sequence1
             for expr2 in sequence2 ...
             for exprN in sequenceN
             if condition ]

for...in 子句包含要迭代的序列。这些序列的长度不必相同,因为它们不是并行迭代的,而是从左到右迭代的;这将在下面的段落中更清楚地解释。生成的列表的元素将是 expression 的连续值。最后的 if 子句是可选的;如果存在,只有当 condition 为真时,才会评估 expression 并将其添加到结果中。

为了使语义非常清晰,列表推导式等效于以下 Python 代码

for expr1 in sequence1:
    for expr2 in sequence2:
    ...
        for exprN in sequenceN:
             if (condition):
                  # Append the value of
                  # the expression to the
                  # resulting list.

这意味着当存在多个 for...in 子句时,生成的列表将等于所有序列的长度的乘积。如果您有两个长度为 3 的列表,则输出列表的长度为 9 个元素

seq1 = 'abc'
seq2 = (1,2,3)
>>> [ (x,y) for x in seq1 for y in seq2]
[('a', 1), ('a', 2), ('a', 3), ('b', 1), ('b', 2), ('b', 3), ('c', 1),
('c', 2), ('c', 3)]

为了避免在 Python 语法中引入歧义,如果 expression 正在创建元组,则必须用括号将其括起来。下面的第一个列表推导式是语法错误,而第二个是正确的

# Syntax error
[ x,y for x in seq1 for y in seq2]
# Correct
[ (x,y) for x in seq1 for y in seq2]

列表推导式的想法最初来自函数式编程语言 Haskell (https://www.haskell.org)。Greg Ewing 最有效地主张将它们添加到 Python 中,并编写了初始的列表推导式补丁,然后在 python-dev 邮件列表上进行了看似无休止的讨论,并由 Skip Montanaro 保持更新。

增强赋值

增强赋值运算符,另一个长期请求的功能,已添加到 Python 2.0 中。增强赋值运算符包括 +=-=*= 等等。例如,语句 a += 2 将变量 a 的值增加 2,等效于稍微长一点的 a = a + 2

支持的赋值运算符的完整列表是 +=-=*=/=%=**=&=|=^=>>=<<=。Python 类可以通过定义名为 __iadd__()__isub__() 等的方法来覆盖增强赋值运算符。例如,以下 Number 类存储一个数字,并支持使用 += 创建一个具有增量值的新实例。

class Number:
    def __init__(self, value):
        self.value = value
    def __iadd__(self, increment):
        return Number( self.value + increment)

n = Number(5)
n += 3
print n.value

__iadd__() 特殊方法使用增量值调用,并且应该返回一个具有适当修改值的新实例;此返回值绑定为左侧变量的新值。

增强赋值运算符最早在 C 编程语言中引入,并且大多数从 C 派生的语言(如 awk、C++、Java、Perl 和 PHP)也支持它们。增强赋值补丁由 Thomas Wouters 实现。

字符串方法

到目前为止,字符串操作功能都在 string 模块中,该模块通常是 C 语言编写的 strop 模块的前端。Unicode 的加入给 strop 模块带来了困难,因为为了接受 8 位字符串或 Unicode 字符串,所有函数都需要重写。对于像 string.replace() 这样接受 3 个字符串参数的函数,这意味着有 8 种可能的排列组合,以及相应复杂的代码。

相反,Python 2.0 将这个问题推给了字符串类型,使得字符串操作功能可以通过 8 位字符串和 Unicode 字符串的方法来使用。

>>> 'andrew'.capitalize()
'Andrew'
>>> 'hostname'.replace('os', 'linux')
'hlinuxtname'
>>> 'moshe'.find('sh')
2

有一件事没有改变,尽管有一个值得注意的愚人节玩笑,那就是 Python 字符串是不可变的。因此,字符串方法返回新的字符串,并且不会修改它们操作的字符串。

旧的 string 模块仍然存在以保持向后兼容性,但它主要作为新字符串方法的前端。

在 2.0 之前的版本中没有对应物,尽管它们在 JPython 中存在了很长时间,但有两个方法是 startswith()endswith()s.startswith(t) 等价于 s[:len(t)] == t,而 s.endswith(t) 等价于 s[-len(t):] == t

另一个值得特别提及的方法是 join()。字符串的 join() 方法接收一个参数,即字符串序列,并且等价于旧的 string 模块中的 string.join() 函数,只是参数反转了。换句话说,s.join(seq) 等价于旧的 string.join(seq, s)

循环垃圾回收

Python 的 C 实现使用引用计数来实现垃圾回收。每个 Python 对象都维护着指向自身的引用数量的计数,并在创建或销毁引用时调整计数。一旦引用计数达到零,该对象就无法再访问,因为您需要拥有对对象的引用才能访问它,并且如果计数为零,则不再存在引用。

引用计数有一些令人愉快的特性:它易于理解和实现,并且生成的实现是可移植的、相当快的,并且与其他实现自己内存处理方案的库反应良好。引用计数的主要问题是它有时无法意识到对象不再可访问,从而导致内存泄漏。当存在循环引用时会发生这种情况。

考虑最简单的循环,即具有对自身引用的类实例

instance = SomeClass()
instance.myself = instance

在执行完上述两行代码后,instance 的引用计数为 2;一个引用来自名为 'instance' 的变量,另一个来自实例的 myself 属性。

如果下一行代码是 del instance,会发生什么?instance 的引用计数减 1,因此其引用计数为 1;myself 属性中的引用仍然存在。然而,该实例无法再通过 Python 代码访问,它可以被删除。如果多个对象彼此引用,它们可以参与到一个循环中,从而导致所有对象都被泄漏。

Python 2.0 通过定期执行循环检测算法来解决这个问题,该算法查找不可访问的循环并删除涉及的对象。新的 gc 模块提供了执行垃圾回收、获取调试统计信息和调整收集器参数的函数。

运行循环检测算法需要一些时间,因此会导致一些额外的开销。希望在从使用 2.0 中获得循环收集的经验后,Python 2.1 能够通过仔细的调整来最大限度地减少开销。目前尚不清楚性能损失了多少,因为对此进行基准测试很棘手,并且主要取决于程序创建和销毁对象的频率。如果在编译 Python 时指定 --without-cycle-gc 开关运行 configure 脚本,则可以禁用循环检测,如果您无法承受哪怕很小的速度损失,或者怀疑循环收集存在错误。

几个人解决了这个问题并为解决方案做出了贡献。Toby Kelsey 编写了循环检测方法的早期实现。目前的算法是由 Eric Tiedemann 在访问 CNRI 期间提出的,Guido van Rossum 和 Neil Schemenauer 编写了两个不同的实现,后来由 Neil 集成。在此过程中,许多其他人提出了建议;python-dev 邮件列表的 2000 年 3 月档案包含了大部分相关讨论,尤其是在名为“Python 的引用循环收集”和“再次终结”的线程中。

其他核心更改

Python 的语法和内置函数进行了一些小的更改。这些更改都不是非常深远的,但它们是很方便的便利性。

次要语言更改

一种新的语法使得使用参数元组和/或关键字参数字典调用给定的函数更加方便。在 Python 1.5 及更早版本中,您会使用 apply() 内置函数:apply(f, args, kw) 使用参数元组 *args* 和字典 *kw* 中的关键字参数调用函数 f()apply() 在 2.0 中是相同的,但由于 Greg Ewing 的一个补丁,f(*args, **kw) 是一种更短、更清晰的方式来实现相同的效果。此语法与定义函数的语法是对称的

def f(*args, **kw):
    # args is a tuple of positional args,
    # kw is a dictionary of keyword args
    ...

现在,print 语句可以通过在 print 后跟 >> file,将输出定向到类文件对象,类似于 Unix shell 中的重定向运算符。以前,您要么必须使用类文件对象的 write() 方法,该方法缺乏 print 的便利性和简单性,要么您可以为 sys.stdout 分配一个新值,然后恢复旧值。对于将输出发送到标准错误,这样写要容易得多

print >> sys.stderr, "Warning: action field not supplied"

现在可以在导入模块时重命名模块,使用语法 import module as namefrom module import name as othername。该补丁由 Thomas Wouters 提交。

使用 % 运算符时,可以使用一种新的格式样式;‘%r’ 将插入其参数的 repr()。这也是出于对称性的考虑而添加的,这次是为了与现有的‘%s’格式样式对称,该样式插入其参数的 str()。例如,'%r %s' % ('abc', 'abc') 返回一个包含 'abc' abc 的字符串。

以前,没有办法实现一个类来覆盖 Python 的内置 in 操作符并实现自定义版本。 obj in seq 如果 obj 存在于序列 seq 中则返回 true;Python 通过简单地尝试序列的每个索引,直到找到 obj 或遇到 IndexError 为止来计算此值。Moshe Zadka 贡献了一个补丁,该补丁添加了一个 __contains__() 魔术方法,用于为 in 提供自定义实现。此外,用 C 编写的新内置对象可以通过序列协议中的新槽来定义 in 对它们意味着什么。

早期版本的 Python 使用递归算法来删除对象。深度嵌套的数据结构可能导致解释器填满 C 堆栈并崩溃;Christian Tismer 重写了删除逻辑以解决此问题。 在一个相关的说明中,比较递归对象会无限递归并崩溃;Jeremy Hylton 重写了代码以不再崩溃,而是产生有用的结果。 例如,在此代码之后

a = []
b = []
a.append(a)
b.append(b)

比较 a==b 返回 true,因为两个递归数据结构是同构的。 请参阅 python-dev 邮件列表 2000 年 4 月存档中的“trashcan 和 PR#7”线程,了解导致此实现的讨论以及一些有用的相关链接。请注意,比较现在也会引发异常。 在早期版本的 Python 中,即使用户定义的 __cmp__() 方法遇到错误,诸如 cmp(a,b) 之类的比较操作也始终会产生答案,因为产生的异常会被静默地吞噬。

ActiveState 的 Trent Mick 主要完成了将 Python 移植到 Itanium 处理器上的 64 位 Windows 的工作。(令人困惑的是,Win64 上的 sys.platform 仍然是 'win32',因为为了便于移植,MS Visual C++ 将 Itanium 上的代码视为 32 位。)PythonWin 还支持 Windows CE;有关更多信息,请参阅 https://pythonce.sourceforge.net/ 上的 Python CE 页面。

另一个新平台是 Darwin/MacOS X;对它的初始支持在 Python 2.0 中。如果你指定“configure –with-dyld –with-suffix=.x”,则动态加载有效。 请参阅 Python 源代码发行版中的 README 以获取更多说明。

已经尝试缓解 Python 的一个缺陷,即当代码在变量被赋值之前引用局部变量时,经常会引起混淆的 NameError 异常。 例如,以下代码在 1.5.2 和 2.0 中的 print 语句上引发异常;在 1.5.2 中,会引发 NameError 异常,而 2.0 会引发新的 UnboundLocalError 异常。 UnboundLocalErrorNameError 的子类,因此任何期望引发 NameError 的现有代码都应该仍然有效。

def f():
    print "i=",i
    i = i + 1
f()

引入了两个新的异常,TabErrorIndentationError。 它们都是 SyntaxError 的子类,当发现 Python 代码的缩进不正确时会引发它们。

内置函数的更改

添加了一个新的内置函数 zip(seq1, seq2, ...)zip() 返回一个元组列表,其中每个元组都包含来自每个参数序列的第 i 个元素。 zip()map(None, seq1, seq2) 之间的区别在于,如果序列的长度不完全相同,map() 会用 None 填充序列,而 zip() 会将返回的列表截断为最短参数序列的长度。

当第一个参数是字符串时,int()long() 函数现在接受可选的“base”参数。 int('123', 10) 返回 123,而 int('123', 16) 返回 291。 int(123, 16) 会引发 TypeError 异常,并显示消息“can't convert non-string with explicit base”。

sys 模块中添加了一个新变量,用于保存更详细的版本信息。 sys.version_info 是一个元组 (major, minor, micro, level, serial)。 例如,在假设的 2.0.1beta1 中,sys.version_info 将是 (2, 0, 1, 'beta', 1)level 是一个字符串,例如 "alpha""beta""final" 用于最终发行版。

字典有一个奇怪的新方法,setdefault(key, default),它的行为与现有的 get() 方法类似。但是,如果缺少键,setdefault() 既会像 get() 一样返回 default 的值,也会将其作为 key 的值插入到字典中。因此,以下几行代码

if dict.has_key( key ): return dict[key]
else:
    dict[key] = []
    return dict[key]

可以简化为单个 return dict.setdefault(key, []) 语句。

解释器设置了最大递归深度,以便在 C 堆栈填满并导致核心转储或 GPF 之前捕获失控的递归。以前,此限制在编译 Python 时是固定的,但在 2.0 中,可以使用 sys.getrecursionlimit()sys.setrecursionlimit() 来读取和修改最大递归深度。默认值为 1000,并且可以通过运行新的脚本 Misc/find_recursionlimit.py 找到给定平台的粗略最大值。

移植到 2.0

新的 Python 版本会尽力与之前的版本兼容,并且记录一直很好。但是,某些更改被认为足够有用,通常是因为它们修复了最初的设计决策,结果被证明是完全错误的,因此有时无法避免破坏向后兼容性。本节列出了 Python 2.0 中可能导致旧 Python 代码中断的更改。

可能破坏最多代码的更改是收紧某些方法接受的参数。某些方法会接受多个参数并将它们视为一个元组,特别是各种列表方法,例如 append()insert()。在早期版本的 Python 中,如果 L 是一个列表,则 L.append( 1,2 ) 会将元组 (1,2) 追加到列表。在 Python 2.0 中,这会导致引发 TypeError 异常,并显示消息:“append requires exactly 1 argument; 2 given”。解决方法是简单地添加一组额外的括号,以将两个值作为元组传递:L.append( (1,2) )

这些方法的早期版本更宽容,因为它们在 Python 的 C 接口中使用了一个旧函数来解析它们的参数;2.0 将它们现代化为使用 PyArg_ParseTuple(),即当前的参数解析函数,它提供更有用的错误消息并将多参数调用视为错误。如果您绝对必须使用 2.0 但无法修复您的代码,您可以编辑 Objects/listobject.c 并定义预处理器符号 NO_STRICT_LIST_APPEND 以保留旧的行为;不建议这样做。

socket 模块中的某些函数在这方面仍然很宽容。例如,socket.connect( ('hostname', 25) ) 是正确的形式,传递一个表示 IP 地址的元组,但是 socket.connect('hostname', 25) 也有效。socket.connect_exsocket.bind 也类似地比较宽松。2.0alpha1 收紧了这些函数,但是由于文档实际上使用了错误的多个参数形式,许多人编写的代码会在更严格的检查下崩溃。面对公众的反应,GvR 撤销了这些更改,因此对于 socket 模块,文档已修复,并且多个参数形式仅被标记为已弃用;在未来的 Python 版本中,它将会再次收紧。

字符串字面量中的 \x 转义现在需要正好 2 个十六进制数字。以前,它会消耗 ‘x’ 之后的所有十六进制数字,并取结果的最低 8 位,因此 \x123456 等同于 \x56

AttributeErrorNameError 异常具有更友好的错误消息,其文本类似于 'Spam' instance has no attribute 'eggs'name 'eggs' is not defined。以前,错误消息只是缺失的属性名称 eggs,并且利用此事实编写的代码将在 2.0 中崩溃。

为了使整数和长整数更具互换性,已经做了一些工作。在 1.5.2 中,为 Solaris 添加了大型文件支持,以允许读取大于 2 GiB 的文件;这使得文件对象的 tell() 方法返回长整数而不是常规整数。一些代码会减去两个文件偏移量,并尝试使用结果来乘以序列或切片字符串,但这会引发 TypeError。在 2.0 中,长整数可以用于乘以或切片序列,并且它的行为正如你直观期望的那样;3L * 'abc' 产生 ‘abcabcabc’,而 (0,1,2,3)[2L:4L] 产生 (2,3)。长整数也可以用于各种以前只接受整数的上下文中,例如文件对象的 seek() 方法,以及 % 运算符(%d%i%x 等)支持的格式。例如,"%d" % 2L**64 将产生字符串 18446744073709551616

最微妙的长整数更改是长整数的 str() 不再有尾随的 ‘L’ 字符,尽管 repr() 仍然包含它。‘L’ 让许多想要打印看起来像普通整数的长整数的人感到恼火,因为他们必须特意去掉这个字符。在 2.0 中这不再是一个问题,但是执行 str(longval)[:-1] 并假设 ‘L’ 在那里的代码现在将丢失最后一位数字。

现在,浮点数的 repr() 使用与 str() 不同的格式精度。repr() 对于 C 的 sprintf() 使用 %.17g 格式字符串,而 str() 像以前一样使用 %.12g。效果是,对于某些数字,repr() 有时可能会显示比 str() 更多的小数位。例如,数字 8.1 不能用二进制精确表示,所以 repr(8.1)'8.0999999999999996',而 str(8.1) 是 '8.1'

-X 命令行选项(它将所有标准异常转换为字符串而不是类)已被删除;标准异常现在将始终是类。包含标准异常的 exceptions 模块已从 Python 转换为内置 C 模块,由 Barry Warsaw 和 Fredrik Lundh 编写。

扩展/嵌入更改

其中一些更改是隐藏的,并且只有编写 C 扩展模块或将 Python 解释器嵌入到更大的应用程序中的人员才会注意到。如果您不处理 Python 的 C API,则可以安全地跳过本节。

Python C API 的版本号已递增,因此必须重新编译为 1.5.2 编译的 C 扩展才能与 2.0 一起使用。在 Windows 上,由于 Windows DLL 的工作方式,Python 2.0 无法导入为 Python 1.5.x 构建的第三方扩展,因此 Python 会引发异常并且导入将失败。

Jim Fulton 的 ExtensionClass 模块的用户会很高兴地发现,已添加了钩子,以便 isinstance()issubclass() 现在支持 ExtensionClass。这意味着您不再需要记住编写诸如 if type(obj) == myExtensionClass 之类的代码,而是可以使用更自然的 if isinstance(obj, myExtensionClass)

Python/importdl.c 文件(这是一个包含 #ifdef 以支持许多不同平台上的动态加载的文件)已由 Greg Stein 清理和重组。importdl.c 现在非常小,并且特定于平台的代码已移至一系列 Python/dynload_*.c 文件中。另一个清理工作:Include/ 目录中还有一些 my*.h 文件,其中包含各种可移植性黑客;它们已合并到一个文件中,Include/pyport.h

Vladimir Marangozov 期待已久的 malloc 重组已完成,以便使 Python 解释器易于使用自定义分配器而不是 C 的标准 malloc()。有关文档,请阅读 Include/pymem.hInclude/objimpl.h 中的注释。对于在其中确定接口的漫长讨论,请参阅 python.org 上 ‘patches’ 和 ‘python-dev’ 列表的网络存档。

最新版本的 MacOS GUSI 开发环境支持 POSIX 线程。因此,Python 的 POSIX 线程支持现在在 Macintosh 上可用。还提供了使用用户空间 GNU pth 库的线程支持。

Windows 上的线程支持也得到了增强。Windows 仅在发生争用时才支持使用内核对象的线程锁;在没有争用的常见情况下,它们使用更简单的函数,这些函数的速度要快一个数量级。在 NT 上,Python 1.5.2 的线程版本比非线程版本慢两倍;通过 2.0 的更改,差异仅为 10%。这些改进由 Yakov Markovitch 贡献。

Python 2.0 的源代码现在仅使用 ANSI C 原型,因此编译 Python 现在需要 ANSI C 编译器,并且不能再使用仅支持 K&R C 的编译器完成。

以前,Python 虚拟机在其字节码中使用 16 位数字,这限制了源文件的大小。特别是,这影响了 Python 源代码中文字列表和字典的最大大小;偶尔,生成 Python 代码的人会遇到此限制。Charles G. Waldman 的补丁将限制从 2**16 提高到 2**32

添加了三个新的便利函数,用于在模块初始化时向模块的字典中添加常量:PyModule_AddObject()PyModule_AddIntConstant()PyModule_AddStringConstant()。这些函数都接受一个模块对象、一个以 null 结尾的 C 字符串(包含要添加的名称)以及一个用于赋值给该名称的第三个参数。这个第三个参数分别是 Python 对象、C long 或 C 字符串。

为 Unix 风格的信号处理程序添加了一个包装 API。PyOS_getsig() 用于获取信号处理程序,而 PyOS_setsig() 用于设置新的处理程序。

Distutils:让模块易于安装

在 Python 2.0 之前,安装模块是一件繁琐的事情——没有办法自动确定 Python 的安装位置,或者为扩展模块使用什么编译器选项。软件作者不得不经历编辑 Makefile 和配置文件的艰苦过程,这些方法只在 Unix 上真正有效,并且不支持 Windows 和 MacOS。Python 用户面临着在不同的扩展包之间差异很大的安装说明,这使得管理 Python 安装成为一件麻烦事。

由 Greg Ward 领导的用于分发实用程序的 SIG 创建了 Distutils,这是一个使包安装更加容易的系统。它们构成了 distutils 包,这是 Python 标准库的一个新部分。在最好的情况下,从源代码安装 Python 模块将需要相同的步骤:首先,您只需解压缩 tarball 或 zip 存档,然后运行“python setup.py install”。平台将被自动检测到,编译器将被识别,C 扩展模块将被编译,并且发行版将被安装到正确的目录中。可选的命令行参数提供对安装过程的更多控制,distutils 包提供了许多地方来覆盖默认值——将构建与安装分开、在非默认目录中构建或安装等等。

为了使用 Distutils,您需要编写一个 setup.py 脚本。对于简单的情况,当软件仅包含 .py 文件时,最小的 setup.py 可以只有几行长

from distutils.core import setup
setup (name = "foo", version = "1.0",
       py_modules = ["module1", "module2"])

如果软件由几个包组成,setup.py 文件也不会复杂太多

from distutils.core import setup
setup (name = "foo", version = "1.0",
       packages = ["package", "package.subpackage"])

C 扩展可能是最复杂的情况;这是一个从 PyXML 包中提取的示例

from distutils.core import setup, Extension

expat_extension = Extension('xml.parsers.pyexpat',
     define_macros = [('XML_NS', None)],
     include_dirs = [ 'extensions/expat/xmltok',
                      'extensions/expat/xmlparse' ],
     sources = [ 'extensions/pyexpat.c',
                 'extensions/expat/xmltok/xmltok.c',
                 'extensions/expat/xmltok/xmlrole.c', ]
       )
setup (name = "PyXML", version = "0.5.4",
       ext_modules =[ expat_extension ] )

Distutils 还可以负责创建源代码和二进制发行版。“sdist”命令由“python setup.py sdist”运行,构建一个源代码发行版,例如 foo-1.0.tar.gz。添加新命令并不困难,“bdist_rpm”和“bdist_wininst”命令已经贡献出来,分别用于为软件创建 RPM 发行版和 Windows 安装程序。创建其他发行版格式(如 Debian 包和 Solaris .pkg 文件)的命令正处于不同的开发阶段。

所有这些都记录在一本新的手册《分发 Python 模块》中,该手册加入了 Python 文档的基本集合。

XML 模块

Python 1.5.2 以 xmllib 模块的形式包含了一个简单的 XML 解析器,该模块由 Sjoerd Mullender 贡献。自 1.5.2 发布以来,两种不同的 XML 处理接口变得很常见:SAX2(简单 XML API 的第 2 版)提供了一个事件驱动的接口,与 xmllib 有些相似之处,而 DOM(文档对象模型)提供了一个基于树的接口,将 XML 文档转换为一个可以遍历和修改的节点树。Python 2.0 包括一个 SAX2 接口和一个精简的 DOM 接口,作为 xml 包的一部分。在这里,我们将简要概述这些新接口;有关完整详细信息,请参阅 Python 文档或源代码。Python XML SIG 也在改进文档。

SAX2 支持

SAX 定义了一个用于解析 XML 的事件驱动接口。要使用 SAX,您必须编写一个 SAX 处理程序类。处理程序类继承自 SAX 提供的各种类,并覆盖 XML 解析器将调用的各种方法。例如,startElement()endElement() 方法在解析器遇到的每个开始和结束标记时被调用,characters() 方法在每个字符数据块时被调用,等等。

事件驱动方法的优点是整个文档不必一次驻留在内存中,这在您处理非常大的文档时很重要。但是,如果您试图以某种复杂的方式修改文档结构,编写 SAX 处理程序类可能会变得非常复杂。

例如,这个小程序定义了一个处理程序,该处理程序为每个开始和结束标记打印一条消息,然后使用它解析文件 hamlet.xml

from xml import sax

class SimpleHandler(sax.ContentHandler):
    def startElement(self, name, attrs):
        print 'Start of element:', name, attrs.keys()

    def endElement(self, name):
        print 'End of element:', name

# Create a parser object
parser = sax.make_parser()

# Tell it what handler to use
handler = SimpleHandler()
parser.setContentHandler( handler )

# Parse a file!
parser.parse( 'hamlet.xml' )

有关更多信息,请参阅 Python 文档,或 https://pyxml.sourceforge.net/topics/howto/xml-howto.html 上的 XML HOWTO。

DOM 支持

文档对象模型是 XML 文档的基于树的表示。顶级的 Document 实例是树的根,并且有一个子节点,它是顶级的 Element 实例。此 Element 具有表示字符数据和任何子元素的子节点,这些子元素可能具有自己的进一步子节点,依此类推。使用 DOM,您可以以您喜欢的任何方式遍历结果树,访问元素和属性值,插入和删除节点,并将树转换回 XML。

DOM 对于修改 XML 文档很有用,因为您可以创建一个 DOM 树,通过添加新节点或重新排列子树来修改它,然后生成一个新的 XML 文档作为输出。您还可以手动构建 DOM 树并将其转换为 XML,这可能比简单地将 <tag1></tag1> 写入文件更灵活地生成 XML 输出。

Python 附带的 DOM 实现位于 xml.dom.minidom 模块中。它是 Level 1 DOM 的轻量级实现,支持 XML 命名空间。提供了 parse()parseString() 便利函数用于生成 DOM 树

from xml.dom import minidom
doc = minidom.parse('hamlet.xml')

doc 是一个 Document 实例。Document,像所有其他的 DOM 类,例如 ElementText,是 Node 基类的子类。因此,DOM 树中的所有节点都支持某些通用方法,例如 toxml(),它返回一个包含节点及其子节点的 XML 表示形式的字符串。每个类也有自己的特殊方法;例如,ElementDocument 实例有一个方法来查找所有具有给定标记名称的子元素。继续前面的两行示例

perslist = doc.getElementsByTagName( 'PERSONA' )
print perslist[0].toxml()
print perslist[1].toxml()

对于 Hamlet XML 文件,以上几行输出

<PERSONA>CLAUDIUS, king of Denmark. </PERSONA>
<PERSONA>HAMLET, son to the late, and nephew to the present king.</PERSONA>

该文档的根元素可用作 doc.documentElement,并且可以通过删除、添加或删除节点来轻松修改其子节点

root = doc.documentElement

# Remove the first child
root.removeChild( root.childNodes[0] )

# Move the new first child to the end
root.appendChild( root.childNodes[0] )

# Insert the new first child (originally,
# the third child) before the 20th child.
root.insertBefore( root.childNodes[0], root.childNodes[20] )

再次,我将您推荐到 Python 文档,以获得不同的 Node 类及其各种方法的完整列表。

与 PyXML 的关系

XML 特殊兴趣小组已经从事 XML 相关的 Python 代码一段时间了。它的代码发行版名为 PyXML,可从 SIG 的网页 https://pythonlang.cn/community/sigs/current/xml-sig 上获得。PyXML 发行版也使用了包名称 xml。如果您编写了使用 PyXML 的程序,您可能想知道它与 2.0 xml 包的兼容性。

答案是,Python 2.0 的 xml 包与 PyXML 不兼容,但可以通过安装最近版本的 PyXML 使其兼容。许多应用程序可以使用 Python 2.0 自带的 XML 支持,但更复杂的应用程序将需要安装完整的 PyXML 包。安装后,PyXML 0.6.0 或更高版本将替换 Python 自带的 xml 包,并且将是标准包的严格超集,添加了许多附加功能。PyXML 中的一些附加功能包括:

  • 4DOM,来自 FourThought, Inc. 的完整 DOM 实现。

  • xmlproc 验证解析器,由 Lars Marius Garshol 编写。

  • sgmlop 解析器加速模块,由 Fredrik Lundh 编写。

模块变更

Python 的扩展标准库进行了许多改进和错误修复;一些受影响的模块包括 readlineConfigParsercgicalendarposixreadlinexmllibaifcchunkwaverandomshelvenntplib。请查阅 CVS 日志以获取逐个补丁的详细信息。

Brian Gallew 为 socket 模块贡献了 OpenSSL 支持。OpenSSL 是安全套接字层的实现,它对通过套接字发送的数据进行加密。在编译 Python 时,您可以编辑 Modules/Setup 以包含 SSL 支持,这会在 socket 模块中添加一个额外的函数:socket.ssl(socket, keyfile, certfile),它接受一个套接字对象并返回一个 SSL 套接字。httpliburllib 模块也进行了更改以支持 https:// URL,尽管没有人实现基于 SSL 的 FTP 或 SMTP。

httplib 模块已由 Greg Stein 重写以支持 HTTP/1.1。

提供了与 1.5 版本 httplib 的向后兼容性,尽管使用诸如流水线之类的 HTTP/1.1 功能将需要重写代码以使用不同的接口集。

Tkinter 模块现在支持 Tcl/Tk 版本 8.1、8.2 或 8.3,并且已删除对旧的 7.x 版本的支持。Tkinter 模块现在支持在 Tk 小部件中显示 Unicode 字符串。此外,Fredrik Lundh 贡献了一个优化,使诸如 create_linecreate_polygon 之类的操作更快,尤其是在使用大量坐标时。

curses 模块已大大扩展,从 Oliver Andrich 的增强版本开始,提供了来自 ncurses 和 SYSV curses 的许多附加功能,例如颜色、备用字符集支持、填充和鼠标支持。这意味着该模块不再与仅具有 BSD curses 的操作系统兼容,但似乎目前没有任何维护的操作系统属于此类别。

如前面关于 2.0 的 Unicode 支持的讨论中所述,re 模块提供的正则表达式的底层实现已更改。SRE 是 Fredrik Lundh 编写的新的正则表达式引擎,并由 Hewlett Packard 部分资助,它支持与 8 位字符串和 Unicode 字符串进行匹配。

新模块

添加了许多新模块。我们将简单地列出它们并附上简短的描述;有关特定模块的详细信息,请查阅 2.0 文档。

  • atexit:用于注册在 Python 解释器退出之前调用的函数。当前直接设置 sys.exitfunc 的代码应更改为使用 atexit 模块,导入 atexit 并使用要在退出时调用的函数调用 atexit.register()。(由 Skip Montanaro 贡献。)

  • codecsencodingsunicodedata:作为新的 Unicode 支持的一部分添加。

  • filecmp:取代了旧的 cmpcmpcachedircmp 模块,这些模块现在已被弃用。(由 Gordon MacMillan 和 Moshe Zadka 贡献。)

  • gettext:此模块通过提供 GNU gettext 消息目录库的接口,为 Python 程序提供国际化 (I18N) 和本地化 (L10N) 支持。(由 Barry Warsaw 从 Martin von Löwis、Peter Funk 和 James Henstridge 的单独贡献中集成。)

  • linuxaudiodev:支持 Linux 上的 /dev/audio 设备,与现有的 sunaudiodev 模块类似。(由 Peter Bosch 贡献,并由 Jeremy Hylton 修复。)

  • mmap:用于 Windows 和 Unix 上的内存映射文件的接口。可以将文件的内容直接映射到内存中,此时它的行为类似于可变字符串,因此可以读取和修改其内容。它们甚至可以传递给期望普通字符串的函数,例如 re 模块。(由 Sam Rushing 贡献,A.M. Kuchling 进行了一些扩展。)

  • pyexpat:Expat XML 解析器的接口。(由 Paul Prescod 贡献。)

  • robotparser:解析 robots.txt 文件,该文件用于编写礼貌地避开网站某些区域的网络爬虫。解析器接受 robots.txt 文件的内容,从中构建一组规则,然后可以回答关于给定 URL 的可抓取性的问题。(由 Skip Montanaro 贡献。)

  • tabnanny:一个用于检查 Python 源代码中不明确缩进的模块/脚本。(由 Tim Peters 贡献。)

  • UserString:一个有用的基类,用于派生行为类似于字符串的对象。

  • webbrowser: 一个模块,提供了一种平台无关的方式来在特定 URL 上启动 Web 浏览器。对于每个平台,会按照特定的顺序尝试各种浏览器。用户可以通过设置 BROWSER 环境变量来更改启动的浏览器。(最初的灵感来自 Eric S. Raymond 对 urllib 的补丁,该补丁添加了类似的功能,但最终的模块来自 Fred Drake 最初作为 Tools/idle/BrowserControl.py 实现的代码,并由 Fred 为标准库改编。)

  • _winreg: Windows 注册表的接口。_winreg 是自 1995 年以来一直是 PythonWin 一部分的功能的改编,但现在已添加到核心发行版中,并增强了对 Unicode 的支持。_winreg 由 Bill Tutt 和 Mark Hammond 编写。

  • zipfile: 用于读取和写入 ZIP 格式存档的模块。这些是由 DOS/Windows 上的 PKZIP 或 Unix 上的 zip 生成的存档,不要与 gzip 格式的文件混淆(gzip 模块支持这些文件)。(由 James C. Ahlstrom 贡献。)

  • imputil: 一个模块,与现有的 ihooks 模块相比,它提供了一种更简单的方法来编写自定义导入钩子。(由 Greg Stein 实现,在 python-dev 上进行了大量讨论。)

IDLE 改进

IDLE 是官方的 Python 跨平台 IDE,使用 Tkinter 编写。Python 2.0 包括 IDLE 0.6,它添加了许多新功能和改进。以下是部分列表

  • UI 改进和优化,尤其是在语法高亮和自动缩进方面。

  • 类浏览器现在显示更多信息,例如模块中的顶级函数。

  • 制表符宽度现在是一个用户可设置的选项。当打开现有的 Python 文件时,IDLE 会自动检测缩进约定并进行调整。

  • 现在支持在各种平台上调用浏览器,用于在浏览器中打开 Python 文档。

  • IDLE 现在有一个命令行,它与普通的 Python 解释器非常相似。

  • 在许多地方添加了调用提示。

  • IDLE 现在可以作为软件包安装。

  • 在编辑器窗口中,底部现在有一个行/列栏。

  • 三个新的快捷键命令:检查模块(Alt-F5),导入模块(F5)和运行脚本(Ctrl-F5)。

删除和弃用的模块

一些模块已被删除,因为它们已过时,或者因为现在有更好的方法来做同样的事情。stdwin 模块已消失;它用于一个不再开发的平台无关的窗口工具包。

许多模块已移动到 lib-old 子目录:cmp, cmpcache, dircmp, dump, find, grep, packmail, poly, util, whatsound, zmod。如果您的代码依赖于已移动到 lib-old 的模块,您可以简单地将该目录添加到 sys.path 以将其恢复,但我们鼓励您更新任何使用这些模块的代码。

致谢

作者要感谢以下人士为本文的各种草稿提供了建议:David Bolen,Mark Hammond,Gregg Hauser,Jeremy Hylton,Fredrik Lundh,Detlef Lannert,Aahz Maruch,Skip Montanaro,Vladimir Marangozov,Tobias Polzin,Guido van Rossum,Neil Schemenauer 和 Russ Schmidt。