Python 2.2 的新特性

作者:

A.M. Kuchling

引言

本文解释了 Python 2.2.2(2002 年 10 月 14 日发布)中的新特性。Python 2.2.2 是 Python 2.2(最初于 2001 年 12 月 21 日发布)的一个错误修复版本。

Python 2.2 可以被认为是“清理版本”。它有一些完全新颖的特性,例如生成器和迭代器,但大多数的更改,尽管意义重大且影响深远,都旨在清理语言设计中不规范和晦涩的角落。

本文不打算提供新特性的完整规范,而是提供一个便捷的概述。有关完整详细信息,您应参阅 Python 2.2 的文档,例如 Python 库参考Python 参考手册。如果您想了解某个特定新特性的完整实现和设计原理,请参阅该特性的 PEP。

PEP 252 和 253:类型和类的变更

Python 2.2 中最大且影响最深远的更改是对 Python 对象和类模型的调整。这些更改应向后兼容,因此您的代码很可能将继续运行不变,但这些更改提供了惊人的新功能。在开始本文最长、最复杂的部分之前,我将概述这些更改并提供一些评论。

很久以前我写了一个网页,列出了 Python 设计中的缺陷。其中一个最显著的缺陷是无法子类化用 C 实现的 Python 类型。特别是,无法子类化内置类型,因此您无法仅仅通过子类化(例如)列表来添加一个有用的方法。UserList 模块提供了一个支持所有列表方法并且可以进一步子类化的类,但是有很多 C 代码期望的是普通的 Python 列表,而不接受 UserList 实例。

Python 2.2 解决了这个问题,并在此过程中增加了一些令人兴奋的新功能。简要总结:

  • 您可以子类化内置类型,例如列表甚至整数,并且您的子类应该在所有需要原始类型的地方正常工作。

  • 现在除了以前版本的 Python 中可用的实例方法外,还可以定义静态方法和类方法。

  • 还可以通过使用一种称为属性(properties)的新机制,在访问或设置实例属性时自动调用方法。许多 __getattr__() 的用法都可以重写为使用属性,从而使生成的代码更简单、更快。作为一个小的好处,属性现在也可以有文档字符串了。

  • 实例的合法属性列表可以使用槽(slots)限制为特定集合,从而可以防止打字错误,并可能在未来版本的 Python 中实现更多优化。

一些用户对所有这些变化表示担忧。他们说,当然,新功能很棒,并且可以实现以前版本的 Python 中不可能实现的各种技巧,但它们也使语言变得更加复杂。一些人表示,他们一直推荐 Python 是因为它简单,并觉得它的简单性正在丧失。

就我个人而言,我认为无需担心。许多新功能都相当深奥,您无需了解它们即可编写大量 Python 代码。编写一个简单的类并不比以前困难,因此除非真正需要,否则您无需学习或教授它们。以前只能通过 C 完成的一些非常复杂的任务,现在可以在纯 Python 中完成,在我看来,这都是更好的。

我不会试图涵盖为使新功能工作所需的所有特殊情况和细微更改。相反,本节只会勾勒出大体轮廓。有关 Python 2.2 新对象模型的更多信息,请参阅“相关链接”一节 相关链接

新旧类

首先,您应该知道 Python 2.2 确实有两种类:经典类或旧式类,以及新式类。旧式类模型与早期 Python 版本中的类模型完全相同。本节描述的所有新特性仅适用于新式类。这种分歧并非旨在永久持续;最终旧式类将被废弃,可能在 Python 3.0 中。

那么如何定义一个新式类呢?您可以通过子类化一个现有新式类来做到这一点。现在大多数 Python 的内置类型,如整数、列表、字典,甚至文件,都是新式类。还增加了一个名为 object 的新式类,它是所有内置类型的基类,因此如果没有合适的内置类型,您只需子类化 object 即可。

class C(object):
    def __init__ (self):
        ...
    ...

这意味着在 Python 2.2 中,不带任何基类的 class 语句始终是经典类。(实际上,您也可以通过设置一个名为 __metaclass__ 的模块级变量来改变这一点——详见 PEP 253 ——但直接子类化 object 更简单。)

内置类型的类型对象作为内置函数提供,使用巧妙的技巧命名。Python 总是拥有名为 int()float()str() 的内置函数。在 2.2 中,它们不再是函数,而是被调用时充当工厂的类型对象。

>>> int
<type 'int'>
>>> int('123')
123

为了使类型集完整,添加了新的类型对象,例如 dict()file()。这是一个更有趣的例子,为文件对象添加了一个 lock() 方法:

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 则可以。

描述符

在以前的 Python 版本中,没有一致的方法来发现对象支持哪些属性和方法。有一些非正式的约定,例如定义 __members____methods__ 属性作为名称列表,但扩展类型或类的作者通常懒得定义它们。您可以回溯到检查对象的 __dict__,但当类继承或任意 __getattr__() 钩子被使用时,这仍然可能不准确。

新类模型背后的一个重要思想是,通过描述符描述对象属性的 API 已经形式化。描述符指定属性的值,说明它是方法还是字段。有了描述符 API,静态方法和类方法成为可能,以及更奇特的构造。

属性描述符是存在于类对象内部的对象,并拥有一些自己的属性:

  • __name__ 是属性的名称。

  • __doc__ 是属性的文档字符串。

  • __get__(object) 是从 object 中检索属性值的方法。

  • __set__(object, value)object 上的属性设置为 value

  • __delete__(object, value) 删除 objectvalue 属性。

例如,当您编写 obj.x 时,Python 实际执行的步骤是:

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

对于方法,descriptor.__get__ 返回一个可调用的临时对象,它封装了实例和要调用的方法。这也是静态方法和类方法现在成为可能的原因;它们有描述符,仅封装方法,或封装方法和类。简单解释这些新类型的方法,静态方法不传递实例,因此类似于常规函数。类方法传递对象的类,但不传递对象本身。静态方法和类方法定义如下:

class C(object):
    def f(arg1, arg2):
        ...
    f = staticmethod(f)

    def g(cls, arg1, arg2):
        ...
    g = classmethod(g)

staticmethod() 函数接受函数 f(),并将其包装在描述符中返回,以便可以将其存储在类对象中。您可能期望有特殊的语法来创建此类方法(def static fdefstatic f() 或类似的东西),但尚未定义此类语法;这已留待未来的 Python 版本。

更多新特性,例如槽(slots)和属性(properties),也作为新型描述符实现,编写一个具有新颖功能的描述符类并不困难。例如,可以编写一个描述符类,使得可以为方法编写 Eiffel 风格的前置条件和后置条件。使用此特性的类可能定义如下:

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 或其他东西,但大多数用户只需在生成的库之上编写代码,而忽略实现细节。

多重继承:菱形法则

通过改变名称解析规则,多重继承也变得更加有用。考虑这组类(图表取自 Guido van Rossum 的 PEP 253):

      class A:
        ^ ^  def save(self): ...
       /   \
      /     \
     /       \
    /         \
class B     class C:
    ^         ^  def save(self): ...
     \       /
      \     /
       \   /
        \ /
      class D

经典类的查找规则很简单,但不够智能;基类是深度优先、从左到右搜索的。对 D.save() 的引用将搜索类 DB,然后是 A,在那里会找到并返回 save()C.save() 根本不会被找到。这很糟糕,因为如果 Csave() 方法正在保存一些特定于 C 的内部状态,不调用它将导致该状态永远不会被保存。

新式类遵循一种不同的算法,解释起来有点复杂,但在这种情况下能正确处理。(请注意,Python 2.3 将此算法更改为在大多数情况下产生相同结果,但对于真正复杂的继承图产生更有用结果的算法。)

  1. 列出所有基类,遵循经典查找规则,如果一个类被重复访问,则将其多次包含在内。在上面的例子中,被访问类的列表是 [D, B, A, C, A]。

  2. 扫描列表以查找重复的类。如果找到任何重复的类,则删除除一个以外的所有出现,只保留列表中最后一个。在上面的例子中,在删除重复项后,列表变为 [D, B, C, A]。

遵循此规则,引用 D.save() 将返回 C.save(),这正是我们想要的行为。此查找规则与 Common Lisp 遵循的规则相同。一个新的内置函数 super() 提供了一种访问类的超类而无需重新实现 Python 算法的方法。最常用的形式是 super(class, obj),它返回一个绑定超类对象(而不是实际的类对象)。此形式将在方法中用于调用超类中的方法;例如,Dsave() 方法将如下所示:

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),但这可能不常有用。

属性访问

相当多的复杂 Python 类使用 __getattr__() 定义属性访问钩子;最常见的是为了方便,通过自动将诸如 obj.parent 的属性访问映射到诸如 obj.get_parent 的方法调用,使代码更具可读性。Python 2.2 增加了一些控制属性访问的新方法。

首先,__getattr__(attr_name) 仍然受新式类支持,并且没有任何改变。与以前一样,当尝试访问 obj.foo 且在实例的字典中未找到名为 foo 的属性时,它将被调用。

新式类也支持一个新方法,__getattribute__(attr_name)。这两个方法的区别在于 __getattribute__() 总是在任何属性被访问时调用,而旧的 __getattr__() 仅在实例的字典中未找到 foo 时调用。

然而,Python 2.2 对属性(properties)的支持通常是捕获属性引用的更简单方法。编写 __getattr__() 方法很复杂,因为为了避免递归,您不能在其中使用常规属性访问,而必须修改 __dict__ 的内容。__getattr__() 方法最终也会在 Python 检查其他方法(例如 __repr__()__coerce__())时被调用,因此必须考虑到这一点来编写。最后,在每次属性访问时调用函数会导致显著的性能损失。

property 是一个新的内置类型,它封装了三个函数来获取、设置或删除属性,以及一个文档字符串。例如,如果您想定义一个可计算但也可设置的 size 属性,您可以这样写:

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")

这显然比一对 __getattr__()/__setattr__() 方法更清晰、更容易编写,这对方法会检查 size 属性并特殊处理,同时从实例的 __dict__ 中检索所有其他属性。对 size 的访问也是唯一需要执行函数调用工作的,因此对其他属性的引用以其通常的速度运行。

最后,可以使用新的 __slots__ 类属性来限制对象上可以引用的属性列表。Python 对象通常非常动态;任何时候都可以通过简单地执行 obj.new_attr=1 来在实例上定义一个新属性。新式类可以定义一个名为 __slots__ 的类属性,将合法属性限制为特定的名称集合。一个例子会清楚地说明这一点:

>>> 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:迭代器

2.2 中另一个重要的补充是在 C 和 Python 级别都引入了迭代接口。对象可以定义调用者如何循环遍历它们。

在 Python 2.1 之前的版本中,使 for item in obj 工作通常是通过定义一个 __getitem__() 方法,其形式如下:

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

__getitem__() 更恰当地用于定义对象的索引操作,以便您可以编写 obj[5] 来检索第六个元素。当您仅使用它来支持 for 循环时,这有点误导。考虑一些想要被循环的文件类对象;index 参数基本上是无意义的,因为类可能假设会以 index 每次递增一的方式进行一系列 __getitem__() 调用。换句话说,__getitem__() 方法的存在并不意味着使用 file[5] 随机访问第六个元素会起作用,尽管它确实应该如此。

在 Python 2.2 中,迭代可以单独实现,并且 __getitem__() 方法可以限制为确实支持随机访问的类。迭代器的基本思想很简单。一个新的内置函数 iter(obj)iter(C, sentinel) 用于获取迭代器。iter(obj) 返回对象 obj 的迭代器,而 iter(C, sentinel) 返回一个迭代器,它将调用可调用对象 C,直到它返回 sentinel 表示迭代器已完成。

Python 类可以定义一个 __iter__() 方法,该方法应该为对象创建并返回一个新的迭代器;如果对象是其自身的迭代器,则此方法可以直接返回 self。特别是,迭代器通常会是其自身的迭代器。用 C 实现的扩展类型可以实现一个 tp_iter 函数来返回一个迭代器,而希望作为迭代器行为的扩展类型可以定义一个 tp_iternext 函数。

那么,经过这一切,迭代器到底做了什么?它们有一个必需的方法,next(),它不接受任何参数并返回下一个值。当没有更多值可返回时,调用 next() 应该抛出 StopIteration 异常。

>>> 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 解释器何时循环遍历序列,它都已更改为使用迭代器协议。这意味着您可以执行以下操作:

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

迭代器支持已添加到 Python 的一些基本类型中。在字典上调用 iter() 将返回一个迭代器,该迭代器遍历其键:

>>> 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() 方法,直到文件中没有更多行。这意味着您现在可以使用以下代码读取文件的每一行:

for line in file:
    # do something for each line
    ...

请注意,您只能在迭代器中向前移动;无法获取前一个元素、重置迭代器或复制它。迭代器对象可以提供此类附加功能,但迭代器协议仅要求一个 next() 方法。

参见

PEP 234 - 迭代器

由 Ka-Ping Yee 和 GvR 撰写;由 Python Labs 团队实现,主要由 GvR 和 Tim Peters 完成。

PEP 255:简单生成器

生成器是另一个新功能,它与迭代器的引入相互作用。

您无疑熟悉 Python 或 C 中函数调用的工作原理。当您调用一个函数时,它会获得一个私有命名空间,其中创建其局部变量。当函数到达 return 语句时,局部变量被销毁,结果值返回给调用者。稍后对同一函数的调用将获得一组全新的局部变量。但是,如果局部变量在函数退出时没有被丢弃呢?如果您可以稍后从函数中断的地方恢复执行呢?这就是生成器提供的功能;它们可以被视为可恢复的函数。

这是一个生成器函数最简单的例子:

def generate_ints(N):
    for i in range(N):
        yield i

为生成器引入了一个新关键字 yield。任何包含 yield 语句的函数都是生成器函数;Python 的字节码编译器会检测到这一点,并因此特殊编译该函数。由于引入了一个新关键字,生成器必须在模块中显式启用,方法是在模块源代码的顶部附近包含 from __future__ import generators 语句。在 Python 2.3 中,此语句将变得不必要。

当你调用一个生成器函数时,它不会返回一个单一的值;相反,它返回一个支持迭代器协议的生成器对象。执行 yield 语句时,生成器输出 i 的值,类似于 return 语句。yieldreturn 语句之间的最大区别在于,当到达 yield 时,生成器的执行状态被挂起,局部变量被保留。在下一次调用生成器的 next() 方法时,函数将紧接着 yield 语句之后恢复执行。(由于复杂的原因,yield 语句不允许出现在 tryfinally 语句的 try 块内;请阅读 PEP 255 以获取 yield 和异常之间交互的完整解释。)

这是一个 generate_ints() 生成器函数的示例用法:

>>> gen = generate_ints(3)
>>> gen
<generator object at 0x8117f90>
>>> gen.next()
0
>>> gen.next()
1
>>> gen.next()
2
>>> gen.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "<stdin>", line 2, in generate_ints
StopIteration

你也可以写 for i in generate_ints(5),或者 a,b,c = generate_ints(3)

在生成器函数内部,return 语句只能在不带值的情况下使用,并表示值序列的结束;此后生成器不能返回任何进一步的值。带有值的 return,例如 return 5,在生成器函数内部是一个语法错误。生成器结果的结束也可以通过手动引发 StopIteration,或者仅仅让执行流程落到函数底部来表示。

您可以通过编写自己的类并将生成器的所有局部变量存储为实例变量来手动实现生成器的效果。例如,返回一个整数列表可以通过将 self.count 设置为 0,并让 next() 方法递增 self.count 并返回它。然而,对于一个中等复杂的生成器,编写相应的类会更加混乱。Lib/test/test_generators.py 包含了一些更有趣的例子。最简单的一个使用递归生成器实现树的按序遍历。

# A recursive generator that generates Tree leaves in in-order.
def inorder(t):
    if t:
        for x in inorder(t.left):
            yield x
        yield t.label
        for x in inorder(t.right):
            yield x

Lib/test/test_generators.py 中的另外两个示例分别给出了 N-皇后问题(在 $NxN$ 棋盘上放置 $N$ 个皇后,使它们互不威胁)和骑士遍历问题(骑士遍历 $NxN$ 棋盘上的所有格子,且不重复访问任何格子)的解决方案。

生成器的思想来源于其他编程语言,尤其是 Icon (https://www2.cs.arizona.edu/icon/),其中生成器的概念是核心。在 Icon 中,每个表达式和函数调用都表现得像一个生成器。来自“Icon 编程语言概述” (https://www2.cs.arizona.edu/icon/docs/ipd266.htm) 的一个例子说明了这一点:

sentence := "Store it in the neighboring harbor"
if (i := find("or", sentence)) > 5 then write(i)

在 Icon 中,find() 函数返回子字符串“or”被找到的索引:3、23、33。在 if 语句中,i 首先被赋值为 3,但 3 小于 5,所以比较失败,Icon 用第二个值 23 重新尝试。23 大于 5,所以比较现在成功,代码将值 23 打印到屏幕上。

Python 在将生成器作为核心概念方面并没有像 Icon 那样深入。生成器被认为是核心 Python 语言的新组成部分,但学习或使用它们并非强制性的;如果它们不能解决您遇到的任何问题,请随意忽略它们。Python 接口与 Icon 相比的一个新颖特性是,生成器的状态表示为一个具体的对象(迭代器),可以传递给其他函数或存储在数据结构中。

参见

PEP 255 - 简单生成器

由 Neil Schemenauer、Tim Peters、Magnus Lie Hetland 撰写。主要由 Neil Schemenauer 和 Tim Peters 实现,并由 Python Labs 团队进行了其他修复。

PEP 237:统一长整数和整数

在最近的版本中,常规整数(在大多数机器上是 32 位值)和长整数(可以是任意大小)之间的区别正变得越来越令人恼火。例如,在支持大于 2**32 字节文件的平台上,文件对象的 tell() 方法必须返回一个长整数。然而,Python 中有各种期望普通整数的代码段,如果提供了长整数,则会引发错误。例如,在 Python 1.5 中,只有常规整数可以用作切片索引,并且 'abc'[1L:] 会引发一个 TypeError 异常,并显示消息“slice index must be int”。

Python 2.2 将根据需要将值从短整数转换为长整数。“L”后缀不再需要指示长整数文字,因为现在编译器将选择适当的类型。(在未来的 Python 2.x 版本中将不鼓励使用“L”后缀,在 Python 2.4 中会触发警告,并可能在 Python 3.0 中取消。)许多以前会引发 OverflowError 的操作现在将返回一个长整数作为结果。例如:

>>> 1234567890123
1234567890123L
>>> 2 ** 64
18446744073709551616L

在大多数情况下,整数和长整数现在将被视为相同。您仍然可以使用内置函数 type() 来区分它们,但这很少需要。

参见

PEP 237 - 统一长整数和整数

由 Moshe Zadka 和 Guido van Rossum 撰写。主要由 Guido van Rossum 实现。

PEP 238:更改除法运算符

Python 2.2 中最具争议的改变预示着一项努力的开始,旨在修复 Python 从一开始就存在的一个旧设计缺陷。目前,Python 的除法运算符 / 在处理两个整数参数时表现得像 C 的除法运算符:它返回一个整数结果,当有小数部分时,该结果会被截断。例如,3/2 是 1,而不是 1.5;(-1)/2 是 -1,而不是 -0.5。这意味着除法结果可能会根据两个操作数的类型而意外地变化,而且由于 Python 是动态类型的,因此很难确定操作数的可能类型。

(争议在于这是否确实是一个设计缺陷,以及是否值得破坏现有代码来修复它。这在 python-dev 上引起了无休止的讨论,并在 2001 年 7 月在 comp.lang.python 上爆发了一场尖酸刻薄的帖子风暴。我在此不为任何一方辩护,只描述 2.2 中实现的机制。请阅读 PEP 238 以获取论点和反论点的总结。)

由于此更改可能会破坏代码,因此它正在逐步引入。Python 2.2 开始了这一过渡,但直到 Python 3.0 才会完全完成切换。

首先,我将借用 PEP 238 中的一些术语。“真除法”是大多数非程序员所熟悉的除法:3/2 是 1.5,1/4 是 0.25,依此类推。“地板除法”是 Python 的 / 运算符在给定整数操作数时当前执行的操作;结果是真除法返回值的地板。“经典除法”是 / 当前的混合行为;当操作数为整数时,它返回地板除法的结果,当其中一个操作数为浮点数时,它返回真除法的结果。

以下是 2.2 引入的更改:

  • 一个新的运算符 // 是地板除法运算符。(是的,我们知道它看起来像 C++ 的注释符号。)// 总是执行地板除法,无论其操作数的类型如何,因此 1 // 2 为 0,1.0 // 2.0 也为 0.0。

    // 在 Python 2.2 中始终可用;您无需使用 __future__ 语句启用它。

  • 通过在模块中包含 from __future__ import division/ 运算符将被更改为返回真除法的结果,因此 1/2 为 0.5。如果没有 __future__ 语句,/ 仍然表示经典除法。/ 的默认含义在 Python 3.0 之前不会改变。

  • 类可以定义名为 __truediv__()__floordiv__() 的方法来重载这两个除法运算符。在 C 级别,PyNumberMethods 结构中也有槽,以便扩展类型可以定义这两个运算符。

  • Python 2.2 支持一些命令行参数,用于测试代码是否与更改后的除法语义兼容。使用 -Q warn 运行 python 将在对两个整数执行除法时发出警告。您可以使用此功能查找受更改影响的代码并进行修复。默认情况下,Python 2.2 将直接执行经典除法而不会发出警告;在 Python 2.3 中,警告将默认开启。

参见

PEP 238 - 更改除法运算符

由 Moshe Zadka 和 Guido van Rossum 撰写。由 Guido van Rossum 实现。

Unicode 变更

Python 的 Unicode 支持在 2.2 中有所增强。Unicode 字符串通常存储为 UCS-2,即 16 位无符号整数。Python 2.2 也可以编译为使用 UCS-4,即 32 位无符号整数作为其内部编码,方法是向 configure 脚本提供 --enable-unicode=ucs4。(也可以指定 --disable-unicode 以完全禁用 Unicode 支持。)

当构建为使用 UCS-4(“宽 Python”)时,解释器可以原生处理从 U+000000 到 U+110000 的 Unicode 字符,因此 unichr() 函数的合法值范围也相应扩大。使用编译为使用 UCS-2(“窄 Python”)的解释器时,大于 65535 的值仍将导致 unichr() 引发 ValueError 异常。这一切都在 PEP 261,“支持‘宽’Unicode 字符”中进行了描述;请查阅它以获取更多详细信息。

另一个改变更容易解释。自引入以来,Unicode 字符串一直支持 encode() 方法,用于将字符串转换为所选编码,如 UTF-8 或 Latin-1。在 2.2 中,8 位字符串(但非 Unicode 字符串)添加了一个对称的 decode([*encoding*]) 方法。decode() 假定字符串采用指定编码并对其进行解码,返回编解码器返回的任何内容。

利用这一新功能,添加了用于与 Unicode 不直接相关的任务的编解码器。例如,已经为 uu 编码、MIME 的 base64 编码以及使用 zlib 模块进行压缩添加了编解码器:

>>> s = """Here is a lengthy piece of redundant, overly verbose,
... and repetitive text.
... """
>>> data = s.encode('zlib')
>>> data
'x\x9c\r\xc9\xc1\r\x80 \x10\x04\xc0?Ul...'
>>> data.decode('zlib')
'Here is a lengthy piece of redundant, overly verbose,\nand repetitive text.\n'
>>> print s.encode('uu')
begin 666 <data>
M2&5R92!I<R!A(&QE;F=T:'D@<&EE8V4@;V8@<F5D=6YD86YT+"!O=F5R;'D@
>=F5R8F]S92P*86YD(')E<&5T:71I=F4@=&5X="X*

end
>>> "sheesh".encode('rot-13')
'furrfu'

要将类实例转换为 Unicode,类可以定义一个 __unicode__() 方法,类似于 __str__()

encode()decode()__unicode__() 由 Marc-André Lemburg 实现。内部支持使用 UCS-4 的更改由 Fredrik Lundh 和 Martin von Löwis 实现。

参见

PEP 261 - 支持“宽”Unicode 字符

由 Paul Prescod 撰写。

PEP 227:嵌套作用域

在 Python 2.1 中,静态嵌套作用域作为可选功能添加,通过 from __future__ import nested_scopes 指令启用。在 2.2 中,嵌套作用域不再需要特殊启用,并且现在总是存在。本节的其余部分是我“Python 2.1 新特性”文档中关于嵌套作用域描述的副本;如果您在 2.1 发布时已阅读过,则可以跳过本节的其余部分。

Python 2.1 中引入并在 2.2 中完成的最大变化是 Python 的作用域规则。在 Python 2.0 中,任何给定时间最多有三个命名空间用于查找变量名:局部、模块级和内置命名空间。这常常令人惊讶,因为它与他们的直观预期不符。例如,嵌套的递归函数定义不起作用:

def f():
    ...
    def g(value):
        ...
        return g(value-1) + 1
    ...

函数 g() 总是会引发 NameError 异常,因为名称 g 的绑定既不在其局部命名空间中,也不在模块级命名空间中。这在实践中并没有太大问题(你多久会这样递归定义内部函数?),但这使得使用 lambda 表达式变得更加笨拙,这在实践中是一个问题。在使用 lambda 的代码中,你经常会发现局部变量通过将它们作为参数的默认值传递而被复制。

def find(self, name):
    "Return list of any entries equal to 'name'"
    L = filter(lambda x, name=name: x == name,
               self.list_attribute)
    return L

因此,以强函数式风格编写的 Python 代码的可读性受到极大影响。

Python 2.2 最重要的变化是语言中增加了静态作用域来解决这个问题。首先,在上面的例子中,name=name 默认参数现在是不必要的。简单来说,当一个给定的变量名在一个函数内部(通过赋值,或 defclassimport 语句)未被赋值时,对该变量的引用将在包含作用域的局部命名空间中查找。关于规则的更详细解释以及实现的剖析,可以在 PEP 中找到。

这种改变可能会对一些代码造成兼容性问题,即在模块级别和函数内部(该函数包含进一步的函数定义)使用相同的变量名。然而,这种情况似乎不太可能发生,因为这样的代码在第一时间阅读起来就已经相当令人困惑了。

这一变化的一个副作用是,在某些条件下,from module import *exec 语句在函数作用域内变得非法。Python 参考手册一直指出 from module import * 仅在模块的顶层合法,但 CPython 解释器以前从未强制执行过这一点。作为嵌套作用域实现的一部分,将 Python 源代码转换为字节码的编译器必须生成不同的代码来访问包含作用域中的变量。from module import *exec 使得编译器无法确定这一点,因为它们向局部命名空间添加了在编译时不可知的名称。因此,如果一个函数包含函数定义或带有自由变量的 lambda 表达式,编译器将通过引发 SyntaxError 异常来标记此问题。

为了使之前的解释更清楚一点,这里有一个例子:

x = 1
def f():
    # The next line is a syntax error
    exec 'x=2'
    def g():
        return x

第 4 行包含 exec 语句是一个语法错误,因为 exec 会定义一个名为 x 的新局部变量,其值应该由 g() 访问。

这不应该是一个很大的限制,因为 exec 在大多数 Python 代码中很少使用(而且当它被使用时,通常也意味着设计不佳)。

参见

PEP 227 - 静态嵌套作用域

由 Jeremy Hylton 撰写和实现。

新增和改进的模块

  • xmlrpclib 模块由 Fredrik Lundh 贡献给标准库,提供了编写 XML-RPC 客户端的支持。XML-RPC 是一个基于 HTTP 和 XML 的简单远程过程调用协议。例如,以下代码片段从 O'Reilly Network 检索 RSS 频道列表,然后列出其中一个频道的最新头条:

    import xmlrpclib
    s = xmlrpclib.Server(
          'http://www.oreillynet.com/meerkat/xml-rpc/server.php')
    channels = s.meerkat.getChannels()
    # channels is a list of dictionaries, like this:
    # [{'id': 4, 'title': 'Freshmeat Daily News'}
    #  {'id': 190, 'title': '32Bits Online'},
    #  {'id': 4549, 'title': '3DGamers'}, ... ]
    
    # Get the items for one channel
    items = s.meerkat.getItems( {'channel': 4} )
    
    # 'items' is another list of dictionaries, like this:
    # [{'link': 'http://freshmeat.net/releases/52719/',
    #   'description': 'A utility which converts HTML to XSL FO.',
    #   'title': 'html2fo 0.3 (Default)'}, ... ]
    

    SimpleXMLRPCServer 模块使得创建简单的 XML-RPC 服务器变得容易。有关 XML-RPC 的更多信息,请参阅 http://xmlrpc.scripting.com/

  • 新的 hmac 模块实现了 RFC 2104 中描述的 HMAC 算法。(由 Gerhard Häring 贡献。)

  • 一些原本返回冗长元组的函数,现在返回伪序列,它们仍然像元组一样工作,但同时具有诸如 memberst_mtimetm_year 等助记属性。这些增强的函数包括 stat()fstat()statvfs()fstatvfs()(在 os 模块中),以及 localtime()gmtime()strptime()(在 time 模块中)。

    例如,要使用旧元组获取文件大小,您最终会写出类似 file_size = os.stat(filename)[stat.ST_SIZE] 的代码,但现在可以更清晰地写成 file_size = os.stat(filename).st_size

    此功能的原始补丁由 Nick Mathewson 贡献。

  • Python 性能分析器已进行了大量重写,并纠正了其输出中的各种错误。(由 Fred L. Drake, Jr. 和 Tim Peters 贡献。)

  • socket 模块可以编译以支持 IPv6;向 Python 的配置脚本指定 --enable-ipv6 选项。(由 Jun-ichiro “itojun” Hagino 贡献。)

  • 在支持 C long long 类型的平台上,struct 模块添加了两个新的格式字符用于 64 位整数。q 用于有符号 64 位整数,Q 用于无符号整数。返回值是 Python 的长整数类型。(由 Tim Peters 贡献。)

  • 在解释器的交互模式中,新增了一个内置函数 help(),它使用 Python 2.1 中引入的 pydoc 模块提供交互式帮助。help(object) 显示关于 object 的任何可用帮助文本。help() 不带参数时,会进入一个在线帮助工具,您可以在其中输入函数、类或模块的名称以阅读其帮助文本。(由 Guido van Rossum 贡献,使用 Ka-Ping Yee 的 pydoc 模块。)

  • re 模块底层 SRE 引擎进行了各种错误修复和性能改进。例如,re.sub()re.split() 函数已用 C 语言重写。另一个贡献的补丁将某些 Unicode 字符范围的速度提高了两倍,并且新增了一个 finditer() 方法,该方法返回给定字符串中所有不重叠匹配的迭代器。(SRE 由 Fredrik Lundh 维护。BIGCHARSET 补丁由 Martin von Löwis 贡献。)

  • smtplib 模块现在支持 RFC 2487,“通过 TLS 的安全 SMTP”,因此现在可以在 Python 程序和处理消息的邮件传输代理之间加密 SMTP 流量。smtplib 还支持 SMTP 身份验证。(由 Gerhard Häring 贡献。)

  • 由 Piers Lauder 维护的 imaplib 模块支持几个新的扩展:RFC 2342 中定义的 NAMESPACE 扩展、SORT、GETACL 和 SETACL。(由 Anthony Baxter 和 Michel Pelletier 贡献。)

  • rfc822 模块对电子邮件地址的解析现在符合 RFC 2822,这是对 RFC 822 的更新。(模块名称不会更改为 rfc2822。)还添加了一个新的包 email,用于解析和生成电子邮件消息。(由 Barry Warsaw 贡献,源于他对 Mailman 的工作。)

  • difflib 模块现在包含一个新的 Differ 类,用于生成两个文本行序列之间人类可读的更改列表(一个“增量”)。还有两个生成器函数 ndiff()restore(),它们分别从两个序列返回一个增量,或从一个增量返回一个原始序列。(由 David Goodger 从 Tim Peters 的 ndiff.py 代码中贡献了繁重的工作,Tim Peters 随后进行了生成器化。)

  • 新的常量 ascii_lettersascii_lowercaseascii_uppercase 已添加到 string 模块中。标准库中有几个模块使用 string.letters 来表示 A-Za-z 范围,但当使用 locale 时,这个假设是不正确的,因为 string.letters 会根据当前 locale 定义的合法字符集而变化。所有有缺陷的模块都已修复为使用 ascii_letters。(由未知人士报告;由 Fred L. Drake, Jr. 修复。)

  • mimetypes 模块现在通过添加一个 MimeTypes 类,使得使用替代 MIME 类型数据库变得更容易,该类接受一个要解析的文件名列表。(由 Fred L. Drake, Jr. 贡献。)

  • threading 模块中添加了一个 Timer 类,允许安排活动在未来的某个时间发生。(由 Itamar Shtull-Trauring 贡献。)

解释器更改和修复

一些更改只影响在 C 级别处理 Python 解释器的人,因为他们正在编写 Python 扩展模块、嵌入解释器或只是修改解释器本身。如果您只编写 Python 代码,这里描述的任何更改都不会对您产生太大影响。

  • 现在可以在 C 语言中实现性能分析和跟踪函数,它们的运行速度比基于 Python 的函数快得多,并且应该减少性能分析和跟踪的开销。这将引起 Python 开发环境作者的兴趣。Python 的 API 中添加了两个新的 C 函数:PyEval_SetProfile()PyEval_SetTrace()。现有的 sys.setprofile()sys.settrace() 函数仍然存在,并且只是被更改为使用新的 C 级接口。(由 Fred L. Drake, Jr. 贡献。)

  • 另一个低级 API,主要针对 Python 调试器和开发工具的实现者,已被添加。PyInterpreterState_Head()PyInterpreterState_Next() 允许调用者遍历所有现有的解释器对象;PyInterpreterState_ThreadHead()PyThreadState_Next() 允许遍历给定解释器的所有线程状态。(由 David Beazley 贡献。)

  • 垃圾回收器的 C 级接口已更改,以便更容易编写支持垃圾回收的扩展类型并调试函数滥用。各种函数的语义略有不同,因此许多函数不得不重命名。使用旧 API 的扩展仍然可以编译,但将参与垃圾回收,因此将其更新到 2.2 应该被视为相当高的优先级。

    要将扩展模块升级到新的 API,请执行以下步骤:

  • Py_TPFLAGS_GC 重命名为 Py_TPFLAGS_HAVE_GC

  • 使用 PyObject_GC_New()PyObject_GC_NewVar() 分配

    对象,并使用 PyObject_GC_Del() 释放它们。

  • PyObject_GC_Init() 重命名为 PyObject_GC_Track(),将 PyObject_GC_Fini() 重命名为 PyObject_GC_UnTrack()

  • 从对象大小计算中删除 PyGC_HEAD_SIZE

  • 删除对 PyObject_AS_GC()PyObject_FROM_GC() 的调用。

  • PyArg_ParseTuple() 添加了一个新的 et 格式序列;et 接受一个参数和一个编码名称,如果参数是 Unicode 字符串,则将其转换为给定编码,如果它是 8 位字符串,则保持不变,假定它已经采用所需的编码。这与 es 格式字符不同,后者假定 8 位字符串采用 Python 的默认 ASCII 编码,并将其转换为指定的新的编码。(由 M.-A. Lemburg 贡献,并用于下一节中描述的 Windows 上的 MBCS 支持。)

  • 添加了一个不同的参数解析函数 PyArg_UnpackTuple(),它更简单且可能更快。调用者无需指定格式字符串,只需提供预期的最小和最大参数数量,以及一组指向 PyObject* 变量的指针,这些变量将被填充参数值。

  • 方法定义表中提供了两个新标志 METH_NOARGSMETH_O,以简化无参数方法或单个无类型参数方法的实现。调用此类方法比调用使用 METH_VARARGS 的相应方法效率更高。此外,编写 C 方法的旧 METH_OLDARGS 样式现已正式弃用。

  • 添加了两个新的包装函数 PyOS_snprintf()PyOS_vsnprintf(),以提供相对较新的 snprintf()vsnprintf() C 库 API 的跨平台实现。与标准的 sprintf()vsprintf() 函数相比,Python 版本会检查用于防止缓冲区溢出的缓冲区边界。(由 M.-A. Lemburg 贡献。)

  • _PyTuple_Resize() 函数已失去一个未使用的参数,因此现在它接受 2 个参数而不是 3 个。第三个参数从未被使用,在将代码从早期版本移植到 Python 2.2 时可以简单地丢弃。

其他更改和修复

像往常一样,整个源代码树中散布着许多其他改进和错误修复。搜索 CVS 更改日志发现,在 Python 2.1 和 2.2 之间应用了 527 个补丁并修复了 683 个错误;2.2.1 应用了 139 个补丁并修复了 143 个错误;2.2.2 应用了 106 个补丁并修复了 82 个错误。这些数字可能被低估了。

一些更显著的更改是

  • 由 Jack Jansen 维护的 Python MacOS 移植代码现在保存在主要的 Python CVS 树中,并且为了支持 MacOS X 进行了许多更改。

    最重要的变化是能够将 Python 构建为一个框架,通过在编译 Python 时向 configure 脚本提供 --enable-framework 选项来实现。根据 Jack Jansen 的说法,“这会将一个自包含的 Python 安装以及 OS X 框架‘粘合’安装到 /Library/Frameworks/Python.framework(或选择的其他位置)。目前,这样做并没有多少直接的额外好处(实际上,有一个缺点是您必须更改 PATH 才能找到 Python),但它是创建完整 Python 应用程序、移植 MacPython IDE、可能将 Python 用作标准 OSA 脚本语言等的基础。”

    MacPython 工具箱模块的大部分,它们与 MacOS API(如窗口、QuickTime、脚本等)接口,已经移植到 OS X,但它们在 setup.py 中被注释掉了。希望试验这些模块的人可以手动取消注释。

  • 传递给不接受它们的内置函数的关键字参数现在会导致引发 TypeError 异常,并显示消息“function takes no keyword arguments”。

  • 弱引用,在 Python 2.1 中作为扩展模块添加,现在是核心的一部分,因为它们用于实现新式类。因此,ReferenceError 异常已从 weakref 模块移出,成为内置异常。

  • Tim Peters 编写的新脚本 Tools/scripts/cleanfuture.py 会自动从 Python 源代码中删除过时的 __future__ 语句。

  • 内置函数 compile() 增加了一个额外的 flags 参数,因此现在可以在模拟 shell(例如 IDLE 和其他开发环境提供的 shell)中正确观察 __future__ 语句的行为。这在 PEP 264 中有描述。(由 Michael Hudson 贡献。)

  • Python 1.6 引入的新许可证与 GPL 不兼容。通过对 2.2 许可证进行一些小的文本更改,此问题已修复,因此现在再次合法地将 Python 嵌入到 GPL 程序中。请注意,Python 本身并非 GPL 许可证,而是采用一种与 BSD 许可证基本相同的许可证,就像它一直以来那样。许可证更改也应用于 Python 2.0.1 和 2.1.1 版本。

  • 当在 Windows 上遇到 Unicode 文件名时,Python 现在会将其转换为 MBCS 编码的字符串,供 Microsoft 文件 API 使用。由于文件 API 明确使用 MBCS,Python 选择 ASCII 作为默认编码被证明是一个麻烦。在 Unix 上,如果 locale.nl_langinfo(CODESET) 可用,则使用 locale 的字符集。(Windows 支持由 Mark Hammond 在 Marc-André Lemburg 的协助下贡献。Unix 支持由 Martin von Löwis 添加。)

  • Windows 上现在支持大文件。(由 Tim Peters 贡献。)

  • Tools/scripts/ftpmirror.py 脚本现在会解析一个 .netrc 文件(如果您有的话)。(由 Mike Romberg 贡献。)

  • xrange() 函数返回的对象的某些特性现在已被弃用,并在访问时触发警告;它们将在 Python 2.3 中消失。xrange 对象试图通过支持切片、序列乘法和 in 运算符来伪装成完整的序列类型,但这些特性很少使用且存在错误。tolist() 方法以及 startstopstep 属性也已被弃用。在 C 级别,PyRange_New() 函数的第四个参数 repeat 也已被弃用。

  • 字典实现中有许多补丁,主要用于修复当字典包含偷偷更改其哈希值或更改其所包含的字典的对象时可能导致的核心转储。有一段时间,python-dev 陷入了一种温和的节奏:Michael Hudson 发现一个导致核心转储的情况,Tim Peters 修复了错误,Michael 又发现另一个情况,如此循环往复。

  • 在 Windows 上,Python 现在可以使用 Borland C 编译,这得益于 Stephen Hansen 贡献的许多补丁,尽管结果尚未完全功能化。(但这确实是进步……)

  • 另一个 Windows 增强:Wise Solutions 大方地向 PythonLabs 提供了他们的 InstallerMaster 8.1 系统。早期的 PythonLabs Windows 安装程序使用 Wise 5.0a,该版本开始显示其年代感。(由 Tim Peters 打包。)

  • .pyw 结尾的文件现在可以在 Windows 上导入。.pyw 是 Windows 专属的,用于指示脚本需要使用 PYTHONW.EXE 而不是 PYTHON.EXE 运行,以防止出现 DOS 控制台显示输出。此补丁使得导入此类脚本成为可能,以防它们也可用作模块。(由 David Bolen 实现。)

  • 在 Python 使用 C dlopen() 函数加载扩展模块的平台上,现在可以使用 sys.getdlopenflags()sys.setdlopenflags() 函数来设置 dlopen() 使用的标志。(由 Bram Stolk 贡献。)

  • 内置函数 pow() 不再支持在提供浮点数时使用 3 个参数。pow(x, y, z) 返回 (x**y) % z,但这对于浮点数从未有用,并且最终结果会根据平台而不可预测地变化。像 pow(2.0, 8.0, 7.0) 这样的调用现在会引发 TypeError 异常。

致谢

作者要感谢以下人员为此文的各个草稿提供了建议、更正和帮助:Fred Bremmer, Keith Briggs, Andrew Dalke, Fred L. Drake, Jr., Carel Fellinger, David Goodger, Mark Hammond, Stephen Hansen, Michael Hudson, Jack Jansen, Marc-André Lemburg, Martin von Löwis, Fredrik Lundh, Michael McLay, Nick Mathewson, Paul Moore, Gustavo Niemeyer, Don O’Donnell, Joonas Paalasma, Tim Peters, Jens Quade, Tom Reinhardt, Neil Schemenauer, Guido van Rossum, Greg Ward, Edward Welbourne。