PEP 234 – 迭代器
- 作者:
- Ka-Ping Yee <ping at zesty.ca>,Guido van Rossum <guido at python.org>
- 状态:
- 最终
- 类型:
- 标准跟踪
- 创建:
- 2001年1月30日
- Python 版本:
- 2.1
- 历史记录:
- 2001年4月30日
摘要
本文档提出了一种迭代接口,对象可以通过该接口提供来控制for
循环的行为。循环是通过提供一个生成迭代器对象的函数来定制的。迭代器提供了一个“获取下一个值”操作,每次调用该操作都会生成序列中的下一个项目,并在没有更多项目可用时引发异常。
此外,还提出了针对字典键和文件行的特定迭代器,并提出了允许将dict.has_key(key)
拼写为key in dict
的建议。
注意:这是第二作者对该 PEP 的几乎完全重写,描述了实际实现(已检入 Python 2.2 CVS 树的主干)。它仍然开放讨论。原始版本中的一些更深奥的建议目前已被撤回;这些可能在将来成为单独的 PEP 的主题。
C API 规范
定义了一个新的异常StopIteration
,可用于表示迭代结束。
在类型对象结构中添加了一个名为tp_iter
的新槽,用于请求迭代器。它应该是一个接受一个PyObject *
参数并返回PyObject *
或NULL
的函数。为了使用此槽,添加了一个新的 C API 函数PyObject_GetIter()
,其签名与tp_iter
槽函数相同。
另一个名为tp_iternext
的新槽被添加到类型结构中,用于获取迭代中的下一个值。为了使用此槽,添加了一个新的 C API 函数PyIter_Next()
。槽和 API 函数的签名如下所示,尽管NULL
返回值条件有所不同:参数是PyObject *
,返回值也是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 中已得到修复。)
为了确保二进制向后兼容性,一个新的标志Py_TPFLAGS_HAVE_ITER
被添加到tp_flags
字段中的标志集以及默认标志宏中。在访问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()
方法,其行为如上所述,以及一个__iter__()
方法,该方法返回self
。
这两种方法对应于两种不同的协议
- 如果对象实现了
__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
:毫无疑问,dict.has_key(x)
对x in dict
的解释是迄今为止最有用的解释,可能是唯一有用的解释。有人对此表示反对,因为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
上次修改时间:2023-09-09 17:39:29 GMT