在Python中,我如何确定一个对象是否可迭代?

1416

有没有像isiterable这样的方法?到目前为止我找到的唯一解决方案是调用

hasattr(myObj, '__iter__')

但是我不确定这有多可靠。


25
__getitem__方法足以使对象可迭代。 - Kos
4
如果您正在查看一个可能是一系列字典或单个字典的 myObj,那么只要 myObj 是字典实例,使用iter(myObj) 就会成功。这是一个微妙之处,如果您想知道什么是序列和什么不是序列很重要。(在Python 2中) - Ben Mosher
11
如果一个对象从零索引开始,那么__getitem__方法也足以使对象可迭代。 - Carlos A. Gómez
__getitem__是可行的,但并非必需。例如,生成器对象是可迭代的,但它没有__getitem__方法。 - Max
__getitem__是可行的,但并非必需。例如,生成器对象是可迭代的,但它没有__getitem__方法。 - undefined
24个回答

1036
  1. 在序列类型中检查 __iter__ 可以工作,但在 Python 2 中例如字符串则会失败。我也想知道正确答案,但在此之前,有一种可能性(也适用于字符串):

    try:
        some_object_iterator = iter(some_object)
    except TypeError as te:
        print(some_object, 'is not iterable')
    

    iter 内置函数会检查 __iter__ 方法或在字符串的情况下检查 __getitem__ 方法。

  2. 另一种通用的 Python 风格是假定一个可迭代对象,如果在给定对象上无法正常工作,则优雅地处理失败。Python 词汇表:

    Pythonic 编程风格通过检查方法或属性签名来确定对象的类型,而不是通过与某些类型对象的显式关系来确定其类型 ("If it looks like a duck and quacks like a duck, it must be a duck.")。通过强调接口而不是特定类型,设计良好的代码通过允许多态替换来提高其灵活性。Duck-typing 避免使用 type() 或 isinstance() 进行测试。相反,它通常采用 EAFP(异常即是 Pythonic)编程风格。

    ...

    try:
       _ = (e for e in my_object)
    except TypeError:
       print(my_object, 'is not iterable')
    
  3. collections 模块提供了一些抽象基类,可以询问类或实例是否提供特定的功能,例如:

    from collections.abc import Iterable
    
    if isinstance(e, Iterable):
        # e is iterable
    

    但是,这不会检查通过 __getitem__ 进行迭代的类。


42
[e for e in my_object] 可能会因为其他原因引发异常,比如 my_object 未定义或在 my_object 实现中存在潜在的错误。 - Nick Dandoulakis
42
字符串一个序列(isinstance('', Sequence) == True),并且和任何序列一样,它可迭代的(isinstance('', Iterable))。虽然 hasattr('', '__iter__') == False,这可能会令人困惑。 - jfs
91
如果my_object非常大(比如像itertools.count()这样的无限对象),使用列表推导式将占用大量时间/内存。更好的方法是创建一个生成器,它永远不会尝试构建一个(潜在无限的)列表。 - Chris Lutz
17
如果some_object因其他原因(例如bug等)而引发了TypeError,该怎么办?我们如何将其与“不可迭代的TypeError”区分开来? - Shaung
65
请注意,在Python 3中:hasattr(u"hello", '__iter__')返回True - Carlos
显示剩余16条评论

701

鸭子类型

try:
    iterator = iter(the_element)
except TypeError:
    # not iterable
else:
    # iterable

# for obj in iterator:
#     pass

类型检查

使用 抽象基类(Abstract Base Classes)。它们至少需要 Python 2.6,并且仅适用于新式类。

from collections.abc import Iterable   # import directly from collections for Python < 3.3

if isinstance(the_element, Iterable):
    # iterable
else:
    # not iterable

然而,根据文档所述:iter()更加可靠:

使用 isinstance(obj, Iterable) 可以检测到已注册为Iterable或具有 __iter__() 方法的类,但无法检测使用 __getitem__() 方法进行迭代的类。唯一确定对象是否可迭代的可靠方法是调用 iter(obj)


54
自Python 3.4起,检查对象x是否可迭代的最准确方法是调用iter(x),并在其不可迭代时处理TypeError异常。这比使用isinstance(x, abc.Iterable) 更准确,因为iter(x)还考虑了遗留的__getitem__方法,而Iterable抽象类则没有。 - RdB
1
@Hi-Angel,听起来像是PyUNO中的一个bug。请注意你的错误信息写成了issubclass()而不是isinstance() - Georg Schölly
4
对一个对象调用iter()可能是一项昂贵的操作(例如在Pytorch中的DataLoader,在其迭代过程中会派生/生成多个进程)。 - szali
2
看起来 enumerate() 与 iter()(Python 3)具有相同的效果,如果您想要做的下一件事是枚举序列,则可能会简化一些事情 - 无需在之前显式使用 iter(),因为如果需要,enumerate() 将自行引发适当的异常。 - Marcin Wojnarski
@szali,那真的只是PyTorch方面的糟糕设计。 - juanpa.arrivillaga
显示剩余5条评论

220

我希望能更详细地介绍 iter__iter____getitem__ 之间的相互作用,以及在幕后发生了什么。掌握了这些知识,您将能够理解为什么最好的选择只能是:

try:
    iter(maybe_iterable)
    print('iteration will probably work')
except TypeError:
    print('not iterable')

首先列出事实,然后简要提醒您在Python中使用for循环时会发生什么,最后讨论以阐明这些事实。

事实

  1. 通过调用iter(o)可以从任何对象o获取迭代器,如果至少满足以下条件之一:

    a)o具有返回迭代器对象的__iter__方法。迭代器是具有__iter____next__(Python 2:next)方法的任何对象。

    b)o具有一个__getitem__方法。

  2. 仅检查IterableSequence的实例,或检查属性__iter__是不够的。

  3. 如果一个对象o仅实现了__getitem__但未实现__iter__,则iter(o)将构造 一个迭代器,该迭代器尝试从索引开始获取o的项目,从索引0开始。迭代器将捕获引发的任何IndexError(但没有其他错误),然后自己引发StopIteration

  4. 最一般地说,除了尝试它之外,没有办法检查由iter返回的迭代器是否正常。

  5. 如果一个对象o实现了__iter__,则iter函数将确保 由__iter__返回的对象是迭代器。如果一个对象仅实现__getitem__,则没有任何健全性检查。

  6. __iter__优先。如果对象o同时实现__iter____getitem__,则iter(o)将调用__iter__

  7. 如果您想使自己的对象可迭代,请始终实现__iter__方法。

for循环

为了跟随本文,您需要了解在Python中使用for循环时会发生什么。如果您已经了解,请直接跳到下一节。

使用 for item in o 遍历可迭代对象 o 时,Python 会调用 iter(o) 并期望返回一个迭代器对象。迭代器是任何实现了 __next__ 方法(在 Python 2 中为 next 方法)和 __iter__ 方法的对象。

按惯例,迭代器的 __iter__ 方法应该返回对象本身(即 return self)。然后,Python 在迭代器上调用 next,直到引发 StopIteration 异常。所有这些都是隐式发生的,但以下演示可以让它变得更加清晰可见:

import random

class DemoIterable(object):
    def __iter__(self):
        print('__iter__ called')
        return DemoIterator()

class DemoIterator(object):
    def __iter__(self):
        return self

    def __next__(self):
        print('__next__ called')
        r = random.randint(1, 10)
        if r == 5:
            print('raising StopIteration')
            raise StopIteration
        return r

遍历 DemoIterable

>>> di = DemoIterable()
>>> for x in di:
...     print(x)
...
__iter__ called
__next__ called
9
__next__ called
8
__next__ called
10
__next__ called
3
__next__ called
10
__next__ called
raising StopIteration

讨论和示例

关于获取迭代器和不可靠的检查:

考虑以下类:

class BasicIterable(object):
    def __getitem__(self, item):
        if item == 3:
            raise IndexError
        return item

当使用 BasicIterable 的实例调用 iter 时,会返回一个迭代器而没有任何问题,因为 BasicIterable 实现了 __getitem__

>>> b = BasicIterable()
>>> iter(b)
<iterator object at 0x7f1ab216e320>

需要注意的是,b 没有 __iter__ 属性,因此不被认为是 IterableSequence 的实例:

>>> from collections import Iterable, Sequence
>>> hasattr(b, '__iter__')
False
>>> isinstance(b, Iterable)
False
>>> isinstance(b, Sequence)
False

这就是为什么Luciano Ramalho在他的书《流畅的Python》中建议调用iter并处理可能出现的TypeError作为检查对象是否可迭代的最准确方法。直接引用该书的原话:

从Python 3.4开始,检查一个对象x是否可迭代的最准确方法是调用iter(x)并处理TypeError异常(如果不可迭代)。这比使用isinstance(x, abc.Iterable)更准确,因为iter(x)还考虑到了旧的__getitem__方法,而Iterable ABC则没有。

关于第三点:迭代仅提供__getitem__但不提供__iter__的对象

BasicIterable实例进行迭代时会按预期工作:Python构建一个迭代器,尝试从零开始获取项目,直到IndexError被触发。演示对象的__getitem__方法只需返回由iter返回的迭代器提供给__getitem__(self, item)item

>>> b = BasicIterable()
>>> it = iter(b)
>>> next(it)
0
>>> next(it)
1
>>> next(it)
2
>>> next(it)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

请注意,当迭代器无法返回下一项时,迭代器会引发StopIteration异常,而针对item==3引发的IndexError异常则在内部处理。这就是为什么使用for循环遍历BasicIterable可以按预期工作的原因:

>>> for x in b:
...     print(x)
...
0
1
2
为了更加深入地阐述iter返回的迭代器如何按索引访问项目的概念,这里举另一个例子。 WrappedDict没有从dict继承,这意味着实例将没有__iter__方法。
class WrappedDict(object): # note: no inheritance from dict!
    def __init__(self, dic):
        self._dict = dic

    def __getitem__(self, item):
        try:
            return self._dict[item] # delegate to dict.__getitem__
        except KeyError:
            raise IndexError
请注意,对__getitem__的调用被委托给dict.__getitem__,方括号表示法只是简写。
>>> w = WrappedDict({-1: 'not printed',
...                   0: 'hi', 1: 'StackOverflow', 2: '!',
...                   4: 'not printed', 
...                   'x': 'not printed'})
>>> for x in w:
...     print(x)
... 
hi
StackOverflow
!

第4和5点:当调用iter时,它会检查是否存在迭代器并调用__iter__方法:

当对一个对象o调用iter(o)时,iter会确保如果方法__iter__存在,则返回值为一个迭代器。这意味着返回的对象必须实现__next__(在Python 2中是next)和__iter__方法。iter不能对仅提供__getitem__方法的对象执行任何合法性检查,因为它无法检查该对象的项是否可通过整数索引访问。

class FailIterIterable(object):
    def __iter__(self):
        return object() # not an iterator

class FailGetitemIterable(object):
    def __getitem__(self, item):
        raise Exception

注意,从 FailIterIterable 实例构建迭代器会立即失败,而从 FailGetItemIterable 构建迭代器成功,但在第一次调用 __next__ 时会抛出异常。

>>> fii = FailIterIterable()
>>> iter(fii)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: iter() returned non-iterator of type 'object'
>>>
>>> fgi = FailGetitemIterable()
>>> it = iter(fgi)
>>> next(it)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/path/iterdemo.py", line 42, in __getitem__
    raise Exception
Exception

第6点:__iter__胜出

这一点很简单。如果一个对象实现了__iter____getitem__,那么iter会调用__iter__。考虑下面的类:

class IterWinsDemo(object):
    def __iter__(self):
        return iter(['__iter__', 'wins'])

    def __getitem__(self, item):
        return ['__getitem__', 'wins'][item]

循环实例时的输出:

>>> iwd = IterWinsDemo()
>>> for x in iwd:
...     print(x)
...
__iter__
wins

第七点要点:您的可迭代类应实现__iter__

您可能会问自己,为什么大多数内置序列(例如list)实现了__iter__方法,而__getitem__已经足够。

class WrappedList(object): # note: no inheritance from list!
    def __init__(self, lst):
        self._list = lst

    def __getitem__(self, item):
        return self._list[item]

毕竟,对上述类的实例进行迭代,将调用__getitem__委托给list.__getitem__(使用方括号表示法),将会正常工作:

>>> wl = WrappedList(['A', 'B', 'C'])
>>> for x in wl:
...     print(x)
... 
A
B
C

自定义可迭代对象需要实现 __iter__ 方法的原因如下:

  1. 如果你实现了 __iter__ 方法,那么该对象将被视为可迭代对象,isinstance(o, collections.abc.Iterable) 将返回 True
  2. 如果 __iter__ 返回的对象不是迭代器,那么调用 iter 函数时会立即抛出 TypeError 异常。
  3. 特殊处理 __getitem__ 的存在是为了向后兼容。引用自《流畅的 Python》:

这就是为什么任何 Python 序列都是可迭代的: 它们都实现了 __getitem__ 。事实上, 标准序列也实现了 __iter__ ,你的也应该这样做,因为特殊处理 __getitem__ 存在于向后兼容性原因,并且在未来可能被删除 (尽管在我写作本文时并未被弃用)。


那么,通过在 try 块中返回 True,并在 except TypeError 块中返回 False,定义一个谓词 is_iterable 是安全的吗? - alancalvitti
这是一个很好的答案。我认为它突显了getitem协议的不直观和不幸的本质。它本应该被避免添加进来。 - Neil G

105

我最近一直在研究这个问题。基于此,我的结论是,现在这是最好的方法:

from collections.abc import Iterable   # drop `.abc` with Python 2.7 or lower

def iterable(obj):
    return isinstance(obj, Iterable)

上面已经推荐过了,但普遍的共识是使用iter()会更好:

def iterable(obj):
    try:
        iter(obj)
    except Exception:
        return False
    else:
        return True

我们在代码中也使用了 iter() 来实现这个目的,但是最近我越来越烦恼的是只有 __getitem__ 的对象被视为可迭代。有一些合法的原因让一个非可迭代对象具有 __getitem__ 属性,而对于这些对象,上述代码并不能很好地工作。我们可以举一个真实的例子,比如 Faker。上述代码报告说它是可迭代的,但实际尝试迭代它会导致一个 AttributeError 错误(在 Faker 4.0.2 中测试过):

>>> from faker import Faker
>>> fake = Faker()
>>> iter(fake)    # No exception, must be iterable
<iterator object at 0x7f1c71db58d0>
>>> list(fake)    # Ooops
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/.../site-packages/faker/proxy.py", line 59, in __getitem__
    return self._factory_map[locale.replace('-', '_')]
AttributeError: 'int' object has no attribute 'replace'
如果我们使用 isinstance(),就不会错误地认为 Faker 实例(或任何只有 __getitem__ 的其他对象)是可迭代的:
>>> from collections.abc import Iterable
>>> from faker import Faker
>>> isinstance(Faker(), Iterable)
False

之前的回答提到使用iter()更安全,因为Python中实现迭代的旧方法是基于__getitem__,而isinstance()方法无法检测到这一点。也许在旧版本的Python中确实如此,但根据我的广泛测试,isinstance()在现在工作得非常好。唯一的情况是UserDict在使用Python 2时isinstance()无法正常工作,但可以使用isinstance(item, (Iterable, UserDict))来解决这个问题。


1
此外,typing.Dictiter(Dict) 视为可迭代对象,但是 list(Dict) 会出现错误 TypeError: Parameters to generic types must be types. Got 0.。如预期的那样,isinstance(Dict, Iterable) 返回 false。 - Pekka Klärck
3
我得出了相同的结论,但原因不同。使用 iter 导致我们的一些代码在使用“预缓存”时不必要地减慢了速度。如果 __iter__ 代码很慢,那么每次只想查看某个东西是否可迭代时调用 iter 的速度也会变慢。 - thorwhalen
3
是否值得在最后一部分添加一条注释,指出 Python 2 不再得到开发人员的积极支持,如果有 Python 3 的选择,不应该再用于新代码? - Gloweye
刚刚发现一个非常奇怪的情况,即 isinstance(obj, Iterable) 失败:numpy 的单个值 'arrays'。如果你有 obj = np.array(int(1)),numpy 会高兴地说 obj = array(1)。形状是一个空元组,而 len(obj) 返回 TypeError: len() of unsized object。然而!如果你写:isinstance(obj, Iterable),你会得到...True - physincubus

46

自从Python 3.5版本起,您可以使用标准库中的typing模块来处理类型相关的事情:

from typing import Iterable

...

if isinstance(my_item, Iterable):
    print(True)

1
这将针对单个字符串对象返回“True”。 - waydegg
4
是的,字符串可迭代。 - Rotareti
6
在 Python 3.6 中,这段代码无法正常工作。在 3.7 版本中它可以正常运行。看起来在 3.9 版本中它将被弃用typing 模块是用于类型检查工具(如 MyPy, PyCharm)的,不能保证此行为。我认为你应该从 collections.abc 模块中导入 Iterable 类。 - c z

32

这还不够:被__iter__返回的对象必须实现迭代协议(即next方法)。请参阅文档中相关部分。

在Python中,一个好的实践是“试一试”而不是“检查”。


11
“duck typing”是我认为您在说的吗? :) (翻译: "duck typing" I believe? :)) - willem
10
@willem:或者说“不要请求许可,而是请求宽恕”;-) - jldupont
14
“允许”和“宽恕”这两种方式都可以归类为"鸭子类型"。如果你问一个对象能做什么而不是它是什么,那就是鸭子类型。如果你使用内省,那就是“允许”,如果你只是尝试去做并查看是否可行,那就是“宽恕”。 - Mark Reed
1
更多关于“不要问Python能为你做什么,而是问你能为Python做什么”的内容。 - mins

23
在 Python 2.5 及以下版本中,你不能也不应该这样做——可迭代对象是一个“非正式”的接口。但自从 Python 2.6 和 3.0 版本开始,你可以利用新的 ABC(抽象基类)架构,并结合一些内置的 ABCs,这些 ABCs 可在 collections 模块中使用:
from collections import Iterable

class MyObject(object):
    pass

mo = MyObject()
print isinstance(mo, Iterable)
Iterable.register(MyObject)
print isinstance(mo, Iterable)

print isinstance("abc", Iterable)

现在,这是否可取或实际起作用只是一种约定俗成的问题。正如您所看到的,您可以将非可迭代对象注册为Iterable,但它会在运行时引发异常。因此,isinstance获得了一个“新”的含义-它只检查“声明”的类型兼容性,这是Python中很好的方法。

另一方面,如果您的对象不满足您需要的接口,你会怎么做?请看以下示例:

from collections import Iterable
from traceback import print_exc

def check_and_raise(x):
    if not isinstance(x, Iterable):
        raise TypeError, "%s is not iterable" % x
    else:
        for i in x:
            print i

def just_iter(x):
    for i in x:
        print i


class NotIterable(object):
    pass

if __name__ == "__main__":
    try:
        check_and_raise(5)
    except:
        print_exc()
        print

    try:
        just_iter(5)
    except:
        print_exc()
        print

    try:
        Iterable.register(NotIterable)
        ni = NotIterable()
        check_and_raise(ni)
    except:
        print_exc()
        print

如果对象不符合您的期望,只需抛出TypeError,但如果已注册适当的ABC,则您的检查是无用的。相反,如果__iter__方法可用,则Python将自动将该类的对象识别为Iterable。

因此,如果您只需要一个可迭代对象,请迭代它并忘记它。另一方面,如果您需要根据输入类型执行不同的操作,则可能会发现ABC基础设施非常有用。


14
在初学者的示例代码中不要使用裸露的 except:,这会促进不良实践。 - jfs
J.F.S:我本不想这样做,但我需要查看多个引发异常的代码,并且我不想捕获特定的异常... 我认为这段代码的目的非常明确。 - Alan Franzoni

21
try:
  #treat object as iterable
except TypeError, e:
  #object is not actually iterable

不要运行检查来判断你的鸭子是否真的是一只鸭子,而应该将其视为可迭代的,并在它不可迭代时报错。


3
在迭代过程中,你的计算可能会抛出 TypeError 异常并使你失去控制,但基本上是这样的。 - Chris Lutz
6
请使用timeit进行基准测试。Python异常通常比if语句更快。它们可以通过解释器走一条略微更短的路径。 - S.Lott
2
@willem:IronPython的异常处理速度较慢(与CPython相比)。 - jfs
2
一个有效的try语句非常快。因此,如果你只有少量的异常,try-except是快速的。如果你预计会有很多异常,“if”可能会更快。 - Arne Babenhauserheide
2
异常对象不应该通过在 TypeError 后添加 "as e" 而不是添加 ", e" 来捕获吗? - HelloGoodbye
显示剩余3条评论

18

你可以尝试这样做:

def iterable(a):
    try:
        (x for x in a)
        return True
    except TypeError:
        return False

如果我们可以创建一个生成器来迭代它(但从不使用该生成器,以便它不占用空间),那么它就是可迭代的。这似乎是“当然”的事情。那么为什么你需要首先确定一个变量是否可迭代呢?


iterable(itertools.repeat(0))怎么样? :) - badp
7
@badp,(x for x in a) 只是创建了一个生成器,它并没有在a上进行任何迭代。 - catchmeifyoutry
5
尝试使用(x for x in a)和尝试iterator = iter(a)两者是否完全等效?或者有一些情况两者是不同的吗? - max
“for _ in a: break” 不是更简单吗?它会更慢吗? - Mr_and_Mrs_D
3
@Mr_and_Mrs_D,如果被测试的对象是后续要迭代的迭代器,则情况很糟糕(由于无法重置其位置,因此其将缺少1个项目),创建垃圾生成器不会迭代对象,因为它们没有被迭代,但我不确定如果非迭代对象是否100%会引发TypeError。 - Tcll

15

到目前为止,我找到的最佳解决方案是:

hasattr(obj, '__contains__')

它基本上检查对象是否实现了in运算符。

优点(其他解决方案都没有全部包括):

  • 它是一个表达式(像lambda一样工作,而不是try...except变体)
  • 它适用于所有可迭代对象,包括字符串(与__iter__相反)
  • 适用于任何Python版本>=2.5

注:

  • Python哲学中的“宁愿请求原谅,不要征得许可”在处理列表时效果不佳,如果你的列表既包括可迭代对象又包括非可迭代对象,并且需要根据它们的类型对每个元素进行不同的处理(例如,在try语句中处理可迭代对象,在except语句中处理非可迭代对象),这种做法看起来会非常丑陋和误导性。
  • 尝试对对象进行实际迭代(例如,[x for x in obj])以检查它是否可迭代的解决方案可能会对大型可迭代对象产生显着的性能损失(特别是如果你只需要可迭代对象的前几个元素),应该避免使用。

3
好的,但为什么不像https://dev59.com/33I-5IYBdhLWcg3wPFx2#1952481中提出的那样使用collections模块呢?对我来说,这样更具表现力。 - Dave Abrahams
1
它更短(且不需要额外的导入),而且没有失去任何清晰度:拥有一个“包含”方法感觉像是检查某个东西是否为对象集合的自然方式。 - Vlad
50
仅仅因为一个对象可以包含另一个对象,并不意味着它一定是可迭代的。例如,用户可以检查一个点是否在一个三维立方体中,但是如何遍历这个对象? - Casey Kuball
16
这是不正确的。在Python 3.4中,可迭代对象本身不支持__contains__。 - Peter Shinners

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