PEP 234 – 迭代器
- 作者:
- Ka-Ping Yee <ping at zesty.ca>, Guido van Rossum <guido at python.org>
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2001-01-30
- Python 版本:
- 2.1
- 发布历史:
- 2001-04-30
摘要
本文档提出了一个迭代接口,对象可以通过该接口控制 for
循环的行为。通过提供一个生成迭代器对象的方法来定制循环。迭代器提供了一个“获取下一个值”的操作,每次调用它都会生成序列中的下一个项,当没有更多项可用时,会引发一个异常。
此外,本文档还提议了针对字典键和文件行的特定迭代器,并提议允许将 dict.has_key(key)
写成 key in dict
。
注意:这是第二作者对本 PEP 的几乎完全重写,描述了实际实现到 Python 2.2 CVS 主干中的内容。它仍然开放讨论。本 PEP 原始版本中一些更深奥的提议已暂时撤回;这些可能会在未来成为单独 PEP 的主题。
C API 规范
定义了一个新的异常,StopIteration
,可用于表示迭代的结束。
类型对象结构中添加了一个名为 tp_iter
的新槽,用于请求迭代器。这应该是一个接受一个 PyObject *
参数并返回一个 PyObject *
或 NULL
的函数。为了使用此槽,添加了一个新的 C API 函数 PyObject_GetIter()
,其签名与 tp_iter
槽函数相同。
类型结构中添加了另一个名为 tp_iternext
的新槽,用于获取迭代中的下一个值。为了使用此槽,添加了一个新的 C API 函数 PyIter_Next()
。槽和 API 函数的签名如下,尽管 NULL
返回条件有所不同:参数是 PyObject *
,返回值也是如此。当返回值非 NULL
时,它是迭代中的下一个值。当它是 NULL
时,对于 tp_iternext slot
,有三种可能性:
- 没有设置异常;这意味着迭代结束。
- 设置了
StopIteration
异常(或派生异常类);这意味着迭代结束。 - 设置了其他异常;这意味着发生了应该正常传播的错误。
更高级别的 PyIter_Next()
函数在 StopIteration
异常(或派生异常)发生时清除它,因此其 NULL
返回条件更简单:
- 没有设置异常;这意味着迭代已结束。
- 设置了某个异常;这意味着发生了错误,并且应该正常传播。
用 C 语言实现的迭代器**不应**实现具有与 tp_iternext
槽相似语义的 next()
方法!当类型字典(通过 PyType_Ready()
)初始化时,存在 tp_iternext
槽会导致一个包装该槽的 next()
方法被添加到类型的 tp_dict
中。(例外:如果类型不使用 PyObject_GenericGetAttr()
访问实例属性,则类型 tp_dict
中的 next()
方法可能不可见。)(由于本 PEP 原始文本中的误解,在 Python 2.2 中,所有迭代器类型都实现了被包装器覆盖的 next()
方法;这已在 Python 2.3 中修复。)
为了确保二进制向后兼容性,在 tp_flags
字段的标志集中以及默认标志宏中添加了一个新标志 Py_TPFLAGS_HAVE_ITER
。在访问 tp_iter
或 tp_iternext
槽之前必须测试此标志。宏 PyIter_Check()
测试对象是否设置了相应的标志并且具有非 NULL
的 tp_iternext
槽。对于 tp_iter
槽没有这样的宏(因为唯一引用此槽的地方应该是 PyObject_GetIter()
,它可以直接检查 Py_TPFLAGS_HAVE_ITER
标志)。
(注意:tp_iter
槽可以存在于任何对象上;tp_iternext
槽应该只存在于充当迭代器的对象上。)
为了向后兼容,PyObject_GetIter()
函数在其参数是不实现 tp_iter
函数的序列时实现回退语义:在这种情况下,会构造一个轻量级序列迭代器对象,该对象以自然顺序迭代序列中的项。
for
循环生成的 Python 字节码已更改为使用新的操作码 GET_ITER
和 FOR_ITER
,它们使用迭代器协议而不是序列协议来获取循环变量的下一个值。这使得可以使用 for
循环来遍历支持 tp_iter
槽的非序列对象。解释器中遍历序列值的其他地方也应更改为使用迭代器。
迭代器应该实现 tp_iter
槽以返回自身的引用;这是为了使得在 for
循环中使用迭代器(而不是序列)成为可能。
迭代器实现(无论是 C 还是 Python)都应该保证,一旦迭代器表示其已耗尽,后续对 tp_iternext
或 next()
方法的调用将继续表示已耗尽。未指定迭代器在引发异常(StopIteration
除外)时是否应进入耗尽状态。请注意,Python 无法保证用户定义或第三方迭代器正确实现此要求。
Python API 规范
StopIteration
异常被列为标准异常之一。它派生自 Exception
。
定义了一个新的内置函数 iter()
,可以通过两种方式调用:
iter(obj)
调用PyObject_GetIter(obj)
。iter(callable, sentinel)
返回一种特殊类型的迭代器,它调用可调用对象生成一个新值,并将返回值与哨兵值进行比较。如果返回值等于哨兵,则表示迭代结束并引发StopIteration
而不是正常返回;如果返回值不等于哨兵,则将其作为迭代器的下一个值返回。如果可调用对象引发异常,则正常传播;特别是,允许函数引发StopIteration
作为结束迭代的另一种方式。(此功能可通过 C API 以PyCallIter_New(callable, sentinel)
形式使用。)
由 iter()
的任何一种形式返回的迭代器对象都具有 next()
方法。此方法要么返回迭代中的下一个值,要么引发 StopIteration
(或派生异常类)以表示迭代结束。任何其他异常都应被视为表示错误,并应正常传播,而不是被视为迭代结束。
类可以通过定义 __iter__()
方法来定义它们的迭代方式;此方法不应接受额外的参数并返回一个有效的迭代器对象。一个想要成为迭代器的类应该实现两个方法:一个 next()
方法(行为如上所述),以及一个返回 self
的 __iter__()
方法。
这两个方法对应两种不同的协议:
- 如果一个对象实现了
__iter__()
或__getitem__()
,则可以使用for
进行迭代。 - 如果一个对象实现了
next()
,则它可以充当迭代器。
类容器对象通常支持协议 1。迭代器目前需要同时支持两种协议。迭代的语义仅来自协议 2;协议 1 的存在是为了让迭代器表现得像序列;特别是为了让接收迭代器的代码可以使用 for 循环遍历迭代器。
字典迭代器
- 字典实现了一个
sq_contains
槽,它实现了与has_key()
方法相同的测试。这意味着我们可以这样写:if k in dict: ...
这等同于
if dict.has_key(k): ...
- 字典实现了一个
tp_iter
槽,它返回一个高效的迭代器,遍历字典的键。在此类迭代期间,字典不应被修改,但允许设置现有键的值(不允许删除或添加,也不允许update()
方法)。这意味着我们可以这样写:for k in dict: ...
这等价于,但比以下方法快得多:
for k in dict.keys(): ...
只要不违反字典修改的限制(无论是通过循环还是通过另一个线程)。
- 向字典添加方法,这些方法显式返回不同类型的迭代器:
for key in dict.iterkeys(): ... for value in dict.itervalues(): ... for key, value in dict.iteritems(): ...
这意味着
for x in dict
是for x in dict.iterkeys()
的简写。
其他映射,如果它们支持迭代器,也应该遍历键。然而,这不应该被视为绝对规则;特定的应用程序可能有不同的要求。
文件迭代器
以下提议很有用,因为它为我们提供了一个很好的答案,解决了“迭代文件行的常用习语既丑陋又缓慢”的抱怨。
- 文件实现了一个
tp_iter
槽,它等价于iter(f.readline, "")
。这意味着我们可以这样写:for line in file: ...
作为以下内容的简写:
for line in iter(file.readline, ""): ...
这等价于,但比以下方法更快:
while 1: line = file.readline() if not line: break ...
这也表明某些迭代器是破坏性的:它们会消耗所有值,并且无法轻易创建第二个独立迭代相同值的迭代器。你可以第二次打开文件,或者 seek()
到开头,但这些解决方案不适用于所有文件类型,例如,当打开的文件对象实际上代表管道或流套接字时,它们不起作用。
因为文件迭代器使用内部缓冲区,所以将其与其他文件操作(例如 file.readline()
)混合使用不会正常工作。此外,以下代码:
for line in file:
if line == "\n":
break
for line in file:
print line,
不会如你所愿地工作,因为第二个 for 循环创建的迭代器没有考虑第一个 for 循环预读的缓冲区。正确的写法是:
it = iter(file)
for line in it:
if line == "\n":
break
for line in it:
print line,
(这些限制的理由是 for line in file
应该成为迭代文件行的推荐标准方式,并且它应该尽可能快。迭代器版本比调用 readline()
快得多,因为它在迭代器内部有缓冲区。)
基本原理
如果包含所有提议部分,这将以一致且灵活的方式解决许多问题。其主要优点包括以下四点——不,五点——不,六点:
- 它提供了一个可扩展的迭代器接口。
- 它允许对列表迭代进行性能增强。
- 它允许对字典迭代进行重大性能增强。
- 它允许提供仅用于迭代的接口,而不假装提供对元素的随机访问。
- 它向后兼容所有现有用户定义的类和扩展对象,这些类和对象模仿序列和映射,甚至是只实现了
__getitem__
、keys
、values
、items
子集的映射。 - 它使遍历非序列集合的代码更简洁易读。
已解决的问题
以下主题已通过共识或 BDFL 公告决定。
- 曾提出两种不同的
next()
拼写方式,但均被拒绝:__next__()
,因为它对应于类型对象槽 (tp_iternext
);以及__call__()
,因为这是唯一的运算。反对
__next__()
的论点:虽然许多迭代器用于 for 循环,但预计用户代码也会直接调用next()
,因此必须编写__next__()
很难看;此外,协议的可能扩展是允许prev()
、current()
和reset()
操作;我们当然不希望使用__prev__()
、__current__()
、__reset__()
。反对
__call__()
(原始提案)的论点:脱离上下文,x()
可读性不佳,而x.next()
则清晰明了;存在一种危险,即每个特殊用途对象都希望为其最常见的操作使用__call__()
,从而导致更多混乱而非清晰。(回想起来,也许选择
__next__()
并提供一个新的内置函数next(it)
来调用it.__next__()
会更好。但事已晚矣;这已于 2001 年 12 月在 Python 2.2 中部署。) - 有些人要求能够重启迭代器。这应该通过重复调用序列上的
iter()
来处理,而不是通过迭代器协议本身。(另请参阅下面请求的扩展。) - 有人质疑用异常来表示迭代结束是否代价过高。针对
StopIteration
异常提出了几种替代方案:使用特殊值End
表示结束,使用函数end()
测试迭代器是否完成,甚至重用IndexError
异常。- 特殊值的问题在于,如果序列中包含该特殊值,循环将过早结束而没有任何警告。如果 C 语言中以空字符结尾的字符串的经验未能告诉我们这可能导致的问题,那么想象一下 Python 内省工具在遍历所有内置名称列表时可能遇到的麻烦,假设特殊的
End
值是一个内置名称! - 调用
end()
函数将需要每次迭代两次调用。两次调用比一次调用加上一次异常测试昂贵得多。特别是时间敏感的 for 循环可以非常廉价地测试异常。 - 重用
IndexError
可能会引起混淆,因为它可能是一个真正的错误,如果循环过早结束,该错误将被掩盖。
- 特殊值的问题在于,如果序列中包含该特殊值,循环将过早结束而没有任何警告。如果 C 语言中以空字符结尾的字符串的经验未能告诉我们这可能导致的问题,那么想象一下 Python 内省工具在遍历所有内置名称列表时可能遇到的麻烦,假设特殊的
- 有些人要求一个标准迭代器类型。大概所有迭代器都必须派生自此类型。但这不符合 Python 的风格:字典之所以是映射,是因为它们支持
__getitem__()
和其他一些操作,而不是因为它们派生自抽象映射类型。 - 关于
if key in dict
:毫无疑问,将x in dict
解释为dict.has_key(x)
是迄今为止最有用的解释,可能也是唯一有用的解释。对此存在抵触,因为x in list
检查 *x* 是否存在于值中,而该提案使x in dict
检查 *x* 是否存在于键中。鉴于列表和字典之间的对称性非常弱,这个论点没有太大分量。 - 名称
iter()
是一个缩写。提出的替代方案包括iterate()
、traverse()
,但这些似乎太长了。Python 有使用缩写作为常见内置函数的历史,例如repr()
、str()
、len()
。决议:就用
iter()
。 - 为两个不同的操作(从对象获取迭代器和为带有哨兵值的函数创建迭代器)使用相同的名称有点难看。不过,我还没有找到第二个操作的更好名称,而且由于它们都返回一个迭代器,所以很容易记住。
决议:内置函数
iter()
接受一个可选参数,即要查找的哨兵。 - 一旦某个迭代器对象引发了
StopIteration
,在随后的所有next()
调用中它也会引发StopIteration
吗?有人认为要求这样做很有用,而另一些人则认为将其留给各个迭代器很有用。请注意,这可能需要某些迭代器实现(例如函数包装迭代器)额外的状态位。决议:一旦引发
StopIteration
,调用it.next()
将继续引发StopIteration
。注:事实上,这在 Python 2.2 中并未实现;在许多情况下,迭代器的
next()
方法可以在一次调用中引发StopIteration
,而在下一次调用中不引发。这已在 Python 2.3 中得到纠正。 - 有人提议文件对象应该成为它自己的迭代器,并带有一个返回下一行的
next()
方法。这有一些优点,并且更清楚地表明此迭代器是破坏性的。缺点是这将使实现前一个要点中提议的“粘性 StopIteration”功能更加痛苦。决议:暂定拒绝(尽管仍有人为此争论不休)。
- 有些人要求扩展迭代器协议,例如
prev()
获取前一个项目,current()
再次获取当前项目,finished()
测试迭代器是否完成,甚至可能还有其他,例如rewind()
,__len__()
,position()
。尽管其中一些很有用,但许多无法在所有迭代器类型上轻松实现,而无需添加任意缓冲,有时它们根本无法实现(或无法合理地实现)。例如,当遍历文件或函数时,任何与反向操作相关的事情都无法完成。也许可以起草一个单独的 PEP 来标准化此类操作在可实现时的名称。
决议:驳回。
- 关于以下问题,曾有过长时间的讨论:
for x in dict: ...
应该将字典的连续键、值或项赋值给 *x*。
if x in y
和for x in y
之间的对称性表明它应该迭代键。这种对称性已被许多人独立观察到,甚至被用来“解释”两者之间的关系。这是因为对于序列,if x in y
遍历 *y* 并将遍历的值与 *x* 进行比较。如果我们采纳上述两个提议,这也将适用于字典。反对将
for x in dict
迭代键的论点主要来自实用性角度:对标准库的扫描显示,for x in dict.items()
的使用次数与for x in dict.keys()
的使用次数大致相同,其中items()
版本略占多数。据推测,许多使用keys()
的循环无论如何都会通过写入dict[x]
来使用相应的值,因此(该论点认为)通过同时提供键和值,我们可以支持最大数量的用例。虽然这是事实,但我(Guido)发现for x in dict
和if x in dict
之间的对应关系太引人注目而无法打破,而且必须写入dict[x]
以显式获取值的开销并不大。要快速遍历项,请使用
for key, value in dict.iteritems()
。我比较了for key in dict: dict[key]
和
for key, value in dict.iteritems(): pass
发现后者只快了大约 7%。
决议:根据 BDFL 的声明,
for x in dict
遍历键,并且字典有iteritems()
、iterkeys()
和itervalues()
来返回不同类型的字典迭代器。
邮件列表
迭代器协议已在 SourceForge 上的邮件列表中进行了广泛讨论:
最初,一些讨论是在 Yahoo 上进行的;存档仍然可访问:
版权
本文档属于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0234.rst