Python 2.2 新特性

作者:

A.M. Kuchling

简介

本文介绍了 2002 年 10 月 14 日发布的 Python 2.2.2 的新特性。Python 2.2.2 是 Python 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 版本中可用的实例方法之外,现在还可以定义静态方法和类方法。

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

  • 可以使用将实例的合法属性列表限制为特定集合,从而可以防止拼写错误,并可能在未来的 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 或其他内容,但大多数用户只需在生成的库之上编写代码,而忽略实现细节。

多重继承:菱形规则

多重继承也通过更改名称解析规则而变得更加有用。考虑以下类集合(图表取自 PEP 253,作者 Guido van Rossum)

      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. 按照经典的查找规则列出所有基类,如果多次访问某个类,则将其多次包含在列表中。在上面的示例中,访问的类列表是 [DBACA]。

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

按照此规则,引用 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__(),而只有在实例的字典中找不到 foo 时才会调用旧的 __getattr__()

然而,Python 2.2 对属性的支持通常是捕获属性引用的更简单方法。编写 __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")

这肯定比一对检查 size 属性并对其进行特殊处理,同时从实例的 __dict__ 中检索所有其他属性的 __getattr__() / __setattr__() 方法更清晰,更容易编写。对 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 级别的迭代接口。对象可以定义调用者如何循环访问它们。

在 2.1 之前的 Python 版本中,使 for item in obj 工作的通常方法是定义一个类似这样的 __getitem__() 方法

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

__getitem__() 更恰当地用于定义对象的索引操作,以便您可以编写 obj[5] 来检索第六个元素。当您仅使用它来支持 for 循环时,这有点误导。考虑一个想要被循环访问的文件类对象;index 参数本质上是无意义的,因为该类可能假设一系列的 __getitem__() 调用每次都会使 index 递增一。换句话说,__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 语句不允许在 try...finally 语句的 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 皇后问题(在 $N\times N$ 棋盘上放置 $N$ 个皇后,使任何一个皇后都不会威胁到另一个皇后)和骑士巡逻问题(骑士在 $N\times N$ 棋盘上不重复访问任何方格的路径)生成解决方案。

生成器的概念来自其他编程语言,特别是 Icon (https://www2.cs.arizona.edu/icon/),其中生成器的概念是核心。在 Icon 中,每个表达式和函数调用都像一个生成器。以下来自 https://www2.cs.arizona.edu/icon/docs/ipd266.htm 的“Icon 编程语言概述”中的一个示例给出了它的外观

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 核心语言的一个新部分,但是学习或使用它们不是强制性的;如果它们没有解决你遇到的任何问题,可以随意忽略它们。与 Icon 相比,Python 的接口的一个新颖之处在于,生成器的状态被表示为一个具体的对象(迭代器),该对象可以传递给其他函数或存储在数据结构中。

另请参阅

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 异常,并显示消息 ‘切片索引必须是 int’。

Python 2.2 将根据需要将值从短整数转换为长整数。不再需要 'L' 后缀来表示长整数字面量,因为现在编译器将选择适当的类型。(在未来 2.x 版本的 Python 中将不鼓励使用 '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 也可以通过向 configure 脚本提供 --enable-unicode=ucs4 来编译为使用 UCS-4(32 位无符号整数)作为其内部编码。(也可以指定 --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

包含 exec 语句的第 4 行是一个语法错误,因为 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 用于无符号的 64 位整数。该值以 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 代码,然后进行了生成器化。)

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

  • 通过添加一个 MimeTypes 类,mimetypes 模块现在可以更轻松地使用备用 MIME 类型数据库,该类接受要解析的文件名列表。(由 Fred L. Drake, Jr. 贡献。)

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

解释器更改和修复

其中一些更改只影响在 C 级别处理 Python 解释器的人员,因为他们正在编写 Python 扩展模块、嵌入解释器或仅仅是在 hack 解释器本身。 如果你只编写 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 lib 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 工具箱模块(与窗口、QuickTime、脚本等 MacOS API 接口)都已移植到 OS X,但它们在 setup.py 中被注释掉了。 想要尝试这些模块的人可以手动取消注释它们。

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

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

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

  • 已向内置函数 compile() 添加了一个额外的 flags 参数,因此现在可以在模拟 shell 中正确观察 __future__ 语句的行为,例如 IDLE 和其他开发环境提供的 shell。 这在 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) 可用,则会使用区域设置的字符集。(Windows 支持由 Mark Hammond 贡献,并得到了 Marc-André Lemburg 的协助。Unix 支持由 Martin von Löwis 添加。)

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

  • 如果存在 .netrc 文件,Tools/scripts/ftpmirror.py 脚本现在会解析该文件。(由 Mike Romberg 贡献。)

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

  • 字典的实现有很多补丁,主要是为了修复当字典包含秘密更改了哈希值或改变了它们所包含的字典的对象时可能出现的崩溃。一段时间内,python-dev 陷入了这样一种温和的节奏:Michael Hudson 发现一个导致崩溃的案例,Tim Peters 修复这个 bug,Michael 又发现另一个案例,如此循环往复。

  • 在 Windows 上,得益于 Stephen Hansen 贡献的一些补丁,Python 现在可以使用 Borland C 进行编译,尽管结果还不完全可用。(但这确实是进步……)

  • 另一个 Windows 增强功能:Wise Solutions 大方地为 PythonLabs 提供了他们的 InstallerMaster 8.1 系统。早期的 PythonLabs Windows 安装程序使用了 Wise 5.0a,它开始显得过时。(由 Tim Peters 打包。)

  • 现在可以在 Windows 上导入以 .pyw 结尾的文件。.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。