如何区分迭代器和可迭代对象?

14
在Python中,可迭代对象的接口是迭代器接口的子集。这有一个好处,在许多情况下,它们可以以相同的方式处理。然而,两者之间有一个重要的语义差别,因为对于可迭代对象,__iter__返回一个新的迭代器对象,而不仅仅是self。如何测试一个可迭代对象确实是一个可迭代对象而不是迭代器?从概念上讲,我理解可迭代对象是集合,而迭代器只管理迭代(即跟踪位置),但本身不是集合。
例如,当想要循环多次时,区别就很重要。如果给定一个迭代器,则第二个循环将无法工作,因为迭代器已经被使用完并直接引发StopIteration
检测next方法似乎很危险且不正确。应该只检查第二个循环是否为空吗?
有没有更符合Python风格的方式来进行这样的测试?我知道这听起来像LBYL与EAFP的经典案例,所以也许我应该放弃?还是我漏掉了什么?
编辑:S.Lott在下面的回答中说,这主要是想要在迭代器上进行多次通行的问题,而且一开始就不应该这样做。然而,在我的情况下,数据非常大,并且根据情况必须多次传递以进行数据处理(绝对没有其他方法)。
可迭代对象也是由用户提供的,对于只需要一次遍历的情况,它可以使用迭代器(例如为了简单起见创建的生成器)。但是,如果需要多次遍历时,最好防止用户仅提供迭代器的情况。
编辑2: 实际上,这是一个非常好的抽象基类示例。迭代器和可迭代对象中的__iter__方法具有相同的名称,但语义上不同!因此,hasattr是无用的,但isinstance提供了一个干净的解决方案。
4个回答

12
'iterator' if obj is iter(obj) else 'iterable'

1
虽然我不知道反例,但这并不能保证可行。 - tzot
1
@ΤΖΩΤΖΙΟΥ:不过,这样的对象有什么意义呢? - vartec
1
我从未说过对象没有.next。您的前提是iter(obj) is obj,在我看来是正确的,但这并不是保证。 - tzot
2
@tzot 根据PEP 234的规定,这是有保障的 - 一个想要成为迭代器的类应该实现两个方法:一个 next() 方法,其行为如上所述,以及一个返回 self 的 __iter__() 方法。 - Piotr Dobrogost
1
答案假设迭代器和可迭代项是不相交的,事实并非如此。 - Piotr Dobrogost
显示剩余6条评论

3
然而,两者之间存在一个重要的语义差异……其实并不是很语义化或重要。它们都是可迭代的——都可以使用for语句工作。
区别在于,比如想要循环多次时就会变得重要。这种情况何时会发生?你需要更具体一些。在极少数需要对可迭代集合进行两次遍历的情况下,通常有更好的算法。
例如,假设正在处理一个列表。你可以随意遍历列表。为什么与迭代器纠缠不清呢?好吧,那行不通。
好的,这里有一个例子。你要在两个阶段读取文件,并且需要知道如何重置可迭代对象。在这种情况下,它是一个文件,需要使用“seek”;或关闭和重新打开。这感觉很不舒服。你可以使用“readlines”获取一个列表,它允许进行两次遍历而没有复杂性。所以这不是必要的。
等等,如果我们有一个太大而无法将其全部读入内存的文件呢?并且,由于某些模糊原因,我们也不能寻找。那怎么办?
现在,我们陷入了两个阶段的细节中。在第一阶段,我们积累了一些东西。一个索引或摘要或其他东西。索引具有文件的所有数据。总结通常是数据的重组。通过从“摘要”到“重组”的小改变,我们在新结构中保留了文件的数据。在这两种情况下,我们不需要文件——我们可以使用索引或摘要。
所有的“两遍”算法都可以改为对原始迭代器或可迭代对象进行一次遍历,然后对不同数据结构进行第二次遍历。
这既不是LYBL也不是EAFP。这是算法设计。你不需要重置迭代器——YAGNI。
编辑
以下是迭代器/可迭代对象问题的一个例子。它只是一个设计不良的算法。
it = iter(xrange(3))
for i in it: print i,; #prints 1,2,3 
for i in it: print i,; #prints nothing

这很容易解决。

it = range(3)
for i in it: print i
for i in it: print i

“多次并行”问题很容易解决。编写一个需要迭代器的API。当有人拒绝阅读API文档或者即使已经阅读了仍然拒绝遵循时,他们的代码就会出错。这是应该的。
“防范用户在需要多次传递时仅提供迭代器”是疯狂程序员编写会破坏我们简单API的代码的例子。
如果有人足够疯狂,已经阅读了大部分API文档但仍然提供了迭代器而不是所需的可迭代对象,你需要找到这个人并教他们(1)如何阅读所有API文档和(2)遵循API文档。
“防范”问题并不是非常现实。这些疯狂的程序员非常罕见。在极少数情况下,当它确实出现时,你知道他们是谁,并可以帮助他们。
“我们必须多次读取相同结构”的算法是一个根本性的问题。不要这样做。
for element in someBigIterable:
    function1( element )
for element in someBigIterable:
    function2( element )
...

请这样做。

for element in someBigIterable:
    function1( element )
    function2( element )
    ...

或者,考虑类似这样的东西。
for element in someBigIterable:
    for f in ( function1, function2, function3, ... ):
        f( element )

在大多数情况下,这种算法的“中心轴”可能会导致更容易优化的程序,并且可能会在性能上有所改善。

4
@S.Lott 那种态度真是讨人厌,认为如果你无法想象需要多次通行的情况,那么这种需求就不值得考虑,或者它不能成为一个有价值的问题的基础。我相信你听说过 Gauss-Seidel 或其他迭代算法来解决线性方程组。你能在一次通行中完成吗?线性方程组也并非高深莫测。这里所需要的只是对问题的明确回答,而不是对 OP 没有提出的糟糕算法的傲慢演讲。 - srean
@srean:这种假设每个人都知道需要多次通过数据的某些算法的态度相当讨厌。可悲的是,很少有人有同样深入的见解。既然我不知道,我就不能提供有意义的评论。你可以提供有用的信息(比如具体的例子,如高斯-塞德尔)。或者你可以抱怨因为我们中的一些人在数学方面的背景不够丰富。 - S.Lott
2
@S.Lott @nikow 对于OP来说,高斯-塞德尔的概念是边缘的,当然有维基百科。最初的问题并不是关于任何多通算法的存在,直到你的自我意识让你相信它是存在的,也没有值得考虑的算法。如果您能回答OP的直接问题,那将更有帮助;如果不能,那么失去一副自以为是的态度,并且避免对OP进行(而且还是错误的)演讲,会更有帮助。 - srean
@srean:对一个例子的无知并不是“自大”。那只是缺乏好的例子而已。“自以为全知”的态度?“向OP讲课”?哇,你肯定对某些事情很生气。很抱歉我的答案让你如此不悦。我认为它们都是关于如何设计算法使得Python中的一次性操作不成问题的好例子。既然你有反例,可以自己写个答案。似乎这比辱骂更有成效。 - S.Lott
2
@S.Lott "你可以自己写答案",我不想在SO上发布无关的答案。无知当然不是自大,但断言或暗示OP可能因为只构思(甚至没有使用)多次通行而有错是错误的。这也是傲慢的,并且在这种情况下也是不正确的。正如你所说,你的例子可能很好,但与OP的问题无关,他的问题是关于可迭代对象和迭代器之间的区别。如果必须要向人们讲解,请不要主动提供态度,无论正确与否,都会产生长远的影响。 - srean
显示剩余16条评论

2
import itertools

def process(iterable):
    work_iter, backup_iter= itertools.tee(iterable)

    for item in work_iter:
        # bla bla
        if need_to_startover():
            for another_item in backup_iter:

那该死的时间机器是Raymond从Guido那里借来的...

0

由于Python的鸭子类型,

如果一个对象定义了next()__iter__()方法并返回自身,则该对象是可迭代的。

如果对象本身没有next()方法,则__iter__()可以返回任何具有next()方法的对象

您可以参考此问题以了解Python中的可迭代性


尝试这个:class A(object): def iter(self): return iter([1,2,3]) def next(self): yield 7 - vartec
实际上,这是鸭子类型的问题:它可以隐藏语义/概念上的差异。它允许我们编写for i in range(3)而不是for i in iter(range(3)),但可能会导致细微的问题。 - nikow
@vartec,你在评论中的代码想要展示什么? - Piotr Dobrogost
@PiotrDobrogost:我不记得了,那已经是3年前的事了;-) - vartec

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接