PEP 201 – 并行迭代
- 作者:
- Barry Warsaw <barry at python.org>
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2000年7月13日
- Python 版本:
- 2.0
- 发布历史:
- 2000 年 7 月 27 日
引言
本 PEP 描述了“并行迭代”提案。本 PEP 跟踪了此功能的状况和所有权,该功能计划在 Python 2.0 中引入。它包含了对该功能的描述,并概述了支持该功能所需的更改。本 PEP 总结了邮件列表论坛中的讨论,并提供了相关 URL 以获取更多信息(如果适用)。此文件的 CVS 修订历史记录包含了确切的历史记录。
动机
Python 中的标准 for 循环会遍历序列中的每个元素,直到序列耗尽 [1]。然而,for 循环只遍历单个序列,而通常需要以并行方式遍历多个序列。换句话说,以这样一种方式,即循环的第 i 次迭代返回一个包含每个序列中第 i 个元素的列表。
用于实现此目的的常见习惯用法并不直观。本 PEP 建议通过引入一个新的内置函数 zip 来执行此类迭代的标准方法。
虽然 zip() 的主要动机来自并行迭代,但通过将 zip() 实现为内置函数,它在 for 循环以外的上下文中具有额外的实用性。
并行 For 循环
并行 for 循环是对两个或更多序列的非嵌套迭代,在循环的每次通过中,从每个序列中取一个元素来组成目标。这种行为已经可以通过使用内置函数 map() 在 Python 中实现
>>> a = (1, 2, 3)
>>> b = (4, 5, 6)
>>> for i in map(None, a, b): print i
...
(1, 4)
(2, 5)
(3, 6)
>>> map(None, a, b)
[(1, 4), (2, 5), (3, 6)]
for 循环像往常一样简单地遍历此列表。
虽然 map() 习语在 Python 中很常见,但它有几个缺点
- 对于没有函数式编程背景的程序员来说,它不明显。
- 使用神奇的
None第一个参数不明显。 - 当列表长度不同时,它具有任意、通常是无意的且不灵活的语义:较短的序列会用
None填充>>> c = (4, 5, 6, 7) >>> map(None, a, c) [(1, 4), (2, 5), (3, 6), (None, 7)]
由于这些原因,在 Python 2.0 测试版期间,提出了几项关于并行 for 循环的语法支持提案。以下是两个建议
for x in seq1, y in seq2:
# stuff
for x, y in seq1, seq2:
# stuff
这两种形式都行不通,因为它们在 Python 中都已有所指代,并且更改含义会破坏现有代码。所有其他关于新语法的建议都存在相同的问题,或者与另一个名为“列表推导式”的提议功能(参见 PEP 202)冲突。
提议的解决方案
提议的解决方案是引入一个新的内置序列生成器函数,可在 __builtin__ 模块中获取。此函数名为 zip,其签名如下
zip(seqa, [seqb, [...]])
zip() 接受一个或多个序列,并将它们的元素编织在一起,就像 map(None, ...) 对等长序列所做的那样。当最短的序列耗尽时,编织停止。
返回值
zip() 返回一个真实的 Python 列表,就像 map() 所做的那样。
示例
以下是一些示例,基于下面的参考实现
>>> a = (1, 2, 3, 4)
>>> b = (5, 6, 7, 8)
>>> c = (9, 10, 11)
>>> d = (12, 13)
>>> zip(a, b)
[(1, 5), (2, 6), (3, 7), (4, 8)]
>>> zip(a, d)
[(1, 12), (2, 13)]
>>> zip(a, b, c, d)
[(1, 5, 9, 12), (2, 6, 10, 13)]
请注意,当序列长度相同时,zip() 是可逆的
>>> a = (1, 2, 3)
>>> b = (4, 5, 6)
>>> x = zip(a, b)
>>> y = zip(*x) # alternatively, apply(zip, x)
>>> z = zip(*y) # alternatively, apply(zip, y)
>>> x
[(1, 4), (2, 5), (3, 6)]
>>> y
[(1, 2, 3), (4, 5, 6)]
>>> z
[(1, 4), (2, 5), (3, 6)]
>>> x == z
1
当序列长度不完全相同时,无法以这种方式反转 zip。
参考实现
以下是 zip() 内置函数的 Python 参考实现。最终批准后将替换为 C 实现
def zip(*args):
if not args:
raise TypeError('zip() expects one or more sequence arguments')
ret = []
i = 0
try:
while 1:
item = []
for s in args:
item.append(s[i])
ret.append(tuple(item))
i = i + 1
except IndexError:
return ret
BDFL 公告
注意:BDFL 指的是 Guido van Rossum,Python 的仁慈独裁者(终身)。
- 函数名称。此 PEP 的早期版本包含一个开放问题,列出了 20 多个提议的
zip()替代名称。在没有压倒性更好选择的情况下,BDFL 强烈倾向于zip(),因为它继承自 Haskell [2]。有关替代方案的列表,请参阅本 PEP 的 1.7 版本。 zip()应为一个内置函数。- 可选填充。本 PEP 的早期版本提出了一个可选的
pad关键字参数,当参数序列长度不一致时使用。这与map(None, ...)的语义类似,只是用户可以指定填充对象。BDFL 拒绝了这一提议,转而始终截断为最短序列,这是因为 KISS 原则。如果确实有需要,以后更容易添加。如果不需要,将来也无法删除它。 - 惰性求值。此 PEP 的早期版本建议
zip()返回一个使用__getitem__()协议执行惰性求值的内置对象。BDFL 强烈反对这一点,转而返回一个真实的 Python 列表。如果将来需要惰性求值,BDFL 建议添加一个xzip()函数。 - 不带参数的
zip()。BDFL 强烈倾向于此情况引发 TypeError 异常。 - 带一个参数的
zip()。BDFL 强烈倾向于此情况返回一个 1 元组列表。 - 内部和外部容器控制。本 PEP 的早期版本包含了对某些人想要的功能的相当长的讨论,即控制内部和外部容器类型(在本 PEP 版本中它们分别是元组和列表)的能力。鉴于简化的 API 和实现,此详细说明被拒绝。有关更详细的分析,请参阅本 PEP 的 1.7 版本。
后续对 zip() 的更改
在 Python 2.4 中,不带参数的 zip() 被修改为返回一个空列表,而不是引发 TypeError 异常。最初行为的理由是,缺少参数被认为表示编程错误。然而,这种想法没有预料到 zip() 与 * 运算符一起使用以解包可变长度参数列表的情况。例如,zip 的逆可以定义为:unzip = lambda s: zip(*s)。这种转换还定义了一个矩阵转置或对于定义为元组列表的表进行等效的行/列交换。后一种转换在读取记录为行、字段为列的数据文件时常用。例如,代码
date, rain, high, low = zip(*csv.reader(file("weather.csv")))
重新排列列式数据,使每个字段都被收集到单独的元组中,以便于循环和汇总
print "Total rainfall", sum(rain)
如果将 zip(*[]) 作为允许的情况而不是异常处理,则 zip(*args) 更容易编码。当数据从空情况构建或递归到空情况(没有记录)时,这尤其有用。
看到这种可能性,BDFL 同意(有些勉强)在 Py2.4 中改变此行为。
其他更改
- 上面讨论的
xzip()函数在 Py2.3 中以itertools.izip()的形式在itertools模块中实现。此函数提供了惰性行为,在每次通过时消耗单个元素并生成单个元组。“及时”样式比其基于列表的对应项zip()节省内存并运行更快。 itertools模块还添加了itertools.repeat()和itertools.chain()。这些工具可以一起使用,用None填充序列(以匹配map(None, seqn)的行为)zip(firstseq, chain(secondseq, repeat(None)))
参考资料
Greg Wilson 对一些 CS 研究生提出的语法进行了问卷调查 https://pythonlang.cn/pipermail/python-dev/2000-July/013139.html
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0201.rst
最后修改时间:2025-02-01 08:55:40 GMT