跟上 Python 的步伐:2.2 版本发布

作者:Wesley J. Chun

Python 2.2 于 2001 年底首次亮相,其首个错误修复版本 2.2.1 最近由 PythonLabs 的核心开发人员发布。2.2.x 系列充满了新特性和功能,其中一些被认为是该语言的重大补充和改进。这些更新在灵活性方面为 Python 开发人员带来了显著的提升。

Python 是一种简单而强大的语言,它结合了脚本工具的易用性和编译型面向对象编程语言的应用程序构建能力。借助 Jython,Python 解释器的 Java 编译版本,Java 程序员正在发现一种工具,可以将他们的生产力和开发速度提升到一个新的水平。

您可以通过阅读 PEP(Python 增强提案)来及时了解这些更改,这些提案旨在倾听 Python 社区的任何合理想法。在考虑对语言进行任何更新之前,会提出问题和拟议的解决方案,以及更改背后的理由和详细信息。您不仅可以在网站上获得 PEP 的确切详细信息(请参阅“资源”),还可以了解 PEP 的状态。在达成共识后,一部分 PEP 获得批准,并计划用于每个版本。例如,2.2 中的更改(意味着整个 2.2.x 版本集)主要由五个主要的 PEP 组成:234、238、252、253 和 255。

首先,2.2 开始统一 Python 整数和长整数的过程。整数计算不再会引发溢出错误,因为如果值溢出,它们将自动转换为长整数。静态嵌套作用域在 2.1 中引入,现在已成为标准,它使 Python 摆脱了其限制性的双作用域模型(请参阅 PEP 227)。以前,必须在脚本的开头放置 from __future__ import nested_scopes 才能启用嵌套作用域。现在,该指令不再必要,因为它已成为标准。Unicode 支持也已升级为 UCS-4(32 位无符号整数;请参阅 PEP 261)。Python 标准库的次要更新包括新的电子邮件包、新的 XML-RPC 模块、向套接字模块添加 IPv6 支持的功能以及新的 hot-shot 性能分析器 (PEP)。

2.2 最重要的变化和新增功能是迭代器和生成器、更改除法运算符以及统一类型和类。

迭代器

迭代器使程序员能够遍历或“迭代”数据集的元素。当这些集合的项目类型不同时,它们尤其有用。Python 已经简化了此编程过程的一部分,因为其序列数据类型(列表、字符串、元组)已经是异构的,并且遍历它们就像“for”循环一样简单,而无需创建任何特殊机制。

Python 中新的迭代支持与 Python 序列无缝协作,但现在也允许程序员迭代非序列类型,包括用户定义的对象。另一个好处是改进了对其他 Python 类型的迭代。

现在,这一切听起来都不错,但是为什么要在 Python 中使用迭代器呢?特别是,PEP 234 指出,此增强功能将

  • 提供可扩展的迭代器接口。

  • 为列表迭代带来性能增强。

  • 允许大幅提高字典迭代的性能。

  • 允许创建真正的迭代接口,而不是覆盖最初用于随机元素访问的方法。

  • 与所有现有的用户定义类和模拟序列和映射的扩展对象向后兼容。

  • 生成更简洁易读的代码,用于迭代非序列集合(例如映射和文件)。

迭代器可以直接使用新的 iter() 内置函数创建,也可以为带有自己迭代接口的对象隐式创建。例如,列表具有内置的迭代接口,因此“for eachItem in myList”将完全不会改变。

调用 iter(obj) 会返回该对象类型的迭代器。迭代器有一个名为 next() 的方法,该方法返回集合中的下一个项目。一个新的异常 StopIteration 表示集合的结束。

但是,迭代器确实有局限性。您无法向后移动、返回开头或复制迭代器。如果要再次(或同时)迭代相同的对象,则必须创建另一个迭代器对象。

序列

如前所述,迭代 Python 序列类型是预期的

>>> myTuple = (123, 'xyz', 45.67)
>>> i = iter(myTuple)
>>> i.next()
123
>>> i.next()
'xyz'
>>> i.next()
45.67
>>> i.next()
Traceback (most recent call last):
  File "", line 1, in ?
StopIteration

如果这是一个实际的程序,我们将把代码放在 try-except 块中。序列现在自动生成自己的迭代器,因此“for”循环

for i in seq:
    do_something_to(i)
在底层现在真的像这样运行
fetch = iter(seq)
while 1:
    try:
        i = fetch.next()
    except StopIteration:
        break
    do_something_to(i)
但是,您的代码不需要更改,因为“for”循环本身会调用迭代器的 next() 方法。

iter() 内置函数还有另一种形式,即 iter(callable, sentinel),它像以前一样返回一个迭代器。不同之处在于,每次调用迭代器的 next() 方法都会调用 callable() 来获取连续的值,并在返回值 sentinel 时引发 StopIteration。

字典

字典和文件是另外两种接受迭代改造的 Python 数据类型。字典的迭代器遍历其键。“for eachKey in myDict.keys()”的惯用语可以缩短为“for eachKey in myDict”,如清单 1 所示。

清单 1. 循环遍历字典

此外,还引入了三个新的内置字典方法来定义迭代:myDict.iterkeys()(迭代键)、myDict.itervalues()(迭代值)和 myDict.iteritems()(迭代键/值对)。请注意,“in”运算符已被修改为检查字典的键。这意味着布尔表达式 myDict.has_key(anyKey) 可以简化为“anyKey in myDict”。

文件

文件对象生成一个调用 readline() 方法的迭代器。因此,它们循环遍历文本文件的所有行,允许程序员将本质上的“for eachLine in myFile.readlines()”替换为更简单的“or eachLine in myFile”

>>> myFile = open('config-win.txt')
>>> for eachLine in myFile:
...     print eachLine,   # comma suppresses extra \n
...
[EditorWindow]
font-name: courier new
font-size: 10
>>> myFile.close()

您还可以为自己的类创建自定义迭代器。这使您可以避免重载 __getitem__() 特殊类方法的黑客行为。重载 __getitem__() 意味着用户可以按任何顺序请求任何下标。但是某些对象在逻辑上不允许这样做。使用迭代器而不是重载 __getitem__() 可以明确用户可以或不能做什么。

要向您的类添加迭代,请重写 __iter__() 特殊方法以返回自身(使对象成为其自身的迭代器)。然后重写 next() 方法

def __iter__(self):
    return self
def next(self):
    # return next item or raise StopIteration

我们可以调整我们的代码以获得类似的示例。这次,我们选择从序列中返回一个随机元素(清单 2)。此示例演示了我们可以使用自定义类迭代执行的一些不寻常的操作。其中之一是无限迭代。因为我们以非破坏性的方式读取序列,所以我们永远不会用完元素,因此我们永远不需要引发 StopIteration。

清单 2. 自定义类迭代

在清单 3 中,我们使用我们的类创建一个迭代器对象,但不是一次迭代一个项目,而是给 next() 方法一个参数,告诉它要返回多少个项目。

清单 3. 使用我们的类创建迭代器对象

现在让我们试用一下

>>> a = AnyIter(range(10))
>>> i = iter(a)
>>> for j in range(1,5):
>>>     print j, ':', i.next(j)
1 : [0]
2 : [1, 2]
3 : [3, 4, 5]
4 : [6, 7, 8, 9]
可变对象和迭代器

在我们继续讨论生成器之前,请记住,在迭代可变对象时干扰它们不是一个好主意。这在迭代器出现之前就是一个问题。一个常见的例子是循环遍历列表并从中删除满足(或不满足)某些标准的项目

for eachURL in allURLs:
    if not eachURL.startswith('http://'):
        allURLs.remove(eachURL)            # YIKES!!

除了列表之外,所有序列都是不可变的,因此危险仅在那里发生。序列的迭代器仅跟踪您所在的第 N 个元素,因此如果您在迭代期间更改元素,这些更新将在您遍历项目时反映出来。如果您用完了,则会引发 StopIteration,但是如果您在末尾添加项目并恢复,则可以继续迭代,如清单 4 所示。

清单 4. 迭代示例

在迭代字典的键时,您不得修改字典。使用字典的 keys() 方法是可以的,因为 keys() 返回一个独立于字典的列表。

但是迭代器与实际对象的关系更加密切,并且不会再让我们玩那个游戏了

>>> myDict = {'a': 1, 'b': 2, 'c': 3, 'd': 4}
>>> for eachKey in myDict:
...   print eachKey, myDict[eachKey]
...   del myDict[eachKey]
...
a 1
Traceback (most recent call last):
  File "", line 1, in ?
RuntimeError: dictionary changed size during
              iteration

这将有助于防止代码出现错误。有关迭代器的完整详细信息,请参阅 PEP 234。

生成器

生成器从迭代器的概念扩展而来。但是,生成器的主要动机来自不同的角度:它们允许跨函数调用保存状态。静态变量,例如在 C 函数中,具有在多次调用该函数时保持其值的能力。这部分解决了状态问题,但真正的好处是像迭代器一样产生一个值,但能够冻结执行,以便在再次调用时准确地从您离开的地方恢复。这正是生成器所做的。它们代表了将迭代与状态以及可恢复的函数合并的想法。当它们这样做时,它们会从上次中断的地方继续,保持它们传递下一个项目所需的所有状态信息完好无损。请注意,我们在此处使用术语 yield 有两个原因:暗示它不是真正的返回(以及帧对象堆栈弹出),并引入新的关键字 yield。

为了向后兼容(以防万一有代码使用 yield 作为标识符),您必须包含“from __future__ import generators”指令才能使用生成器。生成器很快将成为标准(2.3?),因此导入将不是必需的。生成器的行为方式与迭代器类似:当达到真正的返回或函数结束并且没有更多值要 yield 时,会引发 StopIteration 异常。这是一个简单的例子

def simpleGen():
    yield 1
    yield '2 --> punch!'

现在我们有了我们的函数,让我们调用它并获取一个生成器对象

>>> myG = simpleGen()
>>> myG.next()
1
>>> myG.next()
'2 --> punch!'
>>> myG.next()
Traceback (most recent call last):
  File "", line 1, in ?
    myG.next()
StopIteration
或者更恰当地说:for eachItem in simpleGen(): print eachItem。当然,这是一个愚蠢的例子。我的意思是,为什么不为此使用真正的迭代器呢?更多的动机来自于能够迭代需要函数的功能的序列,而不是已经存在于某个序列中的静态对象。

在以下示例中,我们将创建一个随机迭代器,该迭代器接受一个序列并从该序列返回一个随机项

from random import randint
def randIter(seq):
    while len(seq) > 0:
        yield seq.pop(randint(0, len(seq)-1))

不同之处在于,返回的每个项目也从该序列中消耗掉,有点像 list.pop() 和 random.choice() 的组合

>>> for eachItem in randIter([123, 'xyz',
    45.678, 9j]):
...     print eachItem
...
'xyz'
9j
45.678
123
表 1 总结了迭代器和生成器之间的差异。您可以在它们各自的 PEP(234 和 255)中找到有关迭代器和生成器的更多详细信息。

表 1. 迭代器和生成器之间的差异

启动更改除法运算符的过程

这也许是迄今为止 Python 最具争议的更新。有很多优点和缺点,但最终那些相信真除法的人获胜了。为了突出这一变化,让我们定义(或重新定义)一些除法术语及其在整数和浮点操作数中的功能。

经典除法

当使用整数操作数时,经典除法会截断小数点,返回一个整数(请参阅下面的“向下取整除法”部分)。当给定一对浮点操作数时,它会返回实际的浮点商(请参阅“真除法”部分)。这是一个 Python 的除法过去和现在仍然是怎样的示例(实际上是真除法和向下取整除法的混合)

>>> 1 / 2          # perform integer result (floor)
0
>>> 1.0 / 2.0      # returns real quotient
0.5
真除法

在这种情况下,结果应始终是实际的商,而与操作数的类型无关。这是当 Python 3.0 接近现实时即将到来的重大变化。目前,要利用真除法,必须给出 from __future__ import division 指令。一旦发生这种情况,除法运算符 (/) 将仅执行真除法

>>> from __future__ import division
>>>
>>> 1 / 2               # returns real quotient
0.5
>>> 1.0 / 2.0           # returns real quotient
0.5
向下取整除法

已创建一个新的除法运算符 (//),它始终截断小数部分并将其四舍五入到数轴上向左的下一个最小整数,而与操作数的数字类型无关。此运算符从 2.2 开始工作,并且不需要上面的 __future__ 指令。

>>> 1 // 2          # floors result, returns integer
0
>>> 1.0 // 2.0      # floors result, returns float
0.0
>>> -1 // 2         # move left on number line
-1

在不深入探讨此更改的论点的情况下,感觉是 Python 的除法运算符可能从一开始就存在缺陷,尤其因为 Python 是那些不习惯向下取整除法的人的首选编程语言。Guido 在他的“Python 2.2 的新功能”ZPUG 演讲中使用的示例之一是

def velocity(distance, totalTime):
    rate = distance / totalTime
这很糟糕,因为此函数不独立于数字类型。您使用一对浮点数的结果肯定与发送一对整数的结果不同。为了弥合这种二分法,您必须在头脑中解决以下非传递性
>>> 1 == 1.0
1
>>> 2 == 2.0
1
>>> 1 / 2 == 1.0 / 2.0            # classic division
0
如果您使用 Python 的新除法模型,宇宙将再次和平
>>> from __future__ import division
>>> 1 / 2 == 1.0 / 2.0            # true division
1
>>> 1 // 2 == 1.0 // 2.0          # floor division
1
虽然这看起来是正确和应该做的事情,但人们不禁担心它可能导致的代码破坏。幸运的是,Python 开发人员已经牢记这一点,因为此更改在 Python 3.0 之前不会是永久性的,而 Python 3.0 还需要几年时间。那些想要新除法的人可以导入它或使用 -Qnew 命令行选项启动 Python。有一些选项可以打开警告,以便为即将到来的新除法做准备。

您可以从 PEP 238 获取更多信息,但请深入研究 comp.lang.python 档案以了解激烈的辩论。表 2 总结了 Python 各个版本中的除法运算符以及导入除法 (from __future__) 时操作的差异。

表 2. 除法运算符摘要

合并类型和类

合并 Python 类型和类已经存在很长时间了。程序员们感到沮丧地发现,他们无法对现有数据类型(例如列表)进行子类化,以针对其应用程序进行自定义。

要了解更多信息,查看相关的 PEP 和 Guido 专门为那些想要快速掌握新式类的人编写的教程,而无需仔细研究 PEP 中发现的所有复杂细节(请参阅“资源”),这不会有什么坏处。我们还将为您提供一个预告类,该类使用增强的堆栈功能扩展了 Python 列表。

此示例 stack2.py 的灵感来自上面的迭代器示例之一(另请参阅 Core Python Programming 网站上的示例 6.2)。

#!/bin/env python
'stack2.py -- subclasses and extends a list'
class Stack(list):
  def __init__(self, *args):
      list.__init__(self, args)   # call base class
                                  # constructor
  def push(self, *args):
      for eachItem in args:       # can push multiple
          self.append(eachItem)   # items
  def pop(self, n=1):
      if n == 1:                  # pop single item
          return list.pop(self)
      else:                       # pop multiple items
          return [ list.pop(self) for i in range(n) ]

以下是我们从展示我们新发现的功能中获得的输出

>>> from stack2 import Stack
>>> m = Stack(123, 'xyz')
>>> m
[123, 'xyz']
>>> m.push(4.5)
>>> m
[123, 'xyz', 4.5]
>>> m.push(1+2j, 'abc')
>>> m
[123, 'xyz', 4.5, (1+2j), 'abc']
>>> m.pop()
'abc'
>>> m.pop(3)
[(1+2j), 4.5, 'xyz']
>>> m
[123]
除了能够对内置类型进行子类化之外,新式类的其他亮点包括
  • “转换”函数是工厂。

  • 新的 __class__、__dict__ 和 __bases__ 属性。

  • __getattribute__() 特殊方法(比 __getattr__() 更智能)。

  • 类描述符。

  • 类属性。

  • 静态方法。

  • 类方法。

  • 超类方法调用。

  • 协作方法。

  • 新的菱形图名称解析。

  • 使用 Slots 的允许类属性的固定集。

有关新式类以及类型和类的统一的更多信息,请参阅 PEP 252 和 253 以及 Guido 上述教程。

结论

尽管所有这些新功能和弱点解决方案使 Python 在发展道路上走得很远,但有些人声称它们违反了 Python 的简单性。如果您是严格的纯粹主义者,这可能是一个有效的考虑因素。但是,通过最终清除一些烦恼并在语言中添加一些更强大的构造,我们可能比以前更好。这些更改不会对现有代码产生负面影响,而那些确实会产生负面影响的更改,例如除法运算符的更改,至少在一段时间内不是必需的,并且可以实现更轻松的过渡。

最后,请参阅“资源”以获取其他一些高级文档,例如 Andrew Kuchlin 的“Python 2.2 的新功能”以及 Guido 去年秋天在 Python 用户组会议上的一次演讲的幻灯片演示文稿。Python 2.2.1 可以从 Python 语言主页下载。祝您编程愉快!

资源

Keeping Up with Python: the 2.2 Release
电子邮件:cyberweb@rocketmail.com

Wesley J. Chun,《Core Python Programming》的作者,拥有十多年的编程和教学经验。Chun 曾使用 Python 帮助构建 Yahoo! Mail 和 Yahoo! People Search,目前受雇于 Synarc,这是一家临床试验服务公司,该公司利用 Python 开发应用程序,使放射科医生能够进行患者评估。

加载 Disqus 评论