在Python中,如何判断一个对象是否可迭代?

1443 投票
25 回答
765391 浏览
提问于 2025-04-15 17:18

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

hasattr(myObj, '__iter__')

但是我不确定这个方法是否可靠。

25 个回答

227

我想更详细地解释一下 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 循环时发生了什么。如果你已经知道,可以直接跳到下一部分。

当你对某个可迭代对象 o 使用 for item in 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

讨论与示例

关于第 1 和第 2 点:获取迭代器和不可靠的检查

考虑以下类:

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

调用 iter 时,BasicIterable 的实例会毫无问题地返回一个迭代器,因为 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 的 Fluent Python 推荐调用 iter 并处理潜在的 TypeError,作为检查对象是否可迭代的最准确方法。直接引用书中的内容:

截至 Python 3.4,检查对象 x 是否可迭代的最准确方法是调用 iter(x),如果不是,则处理 TypeError 异常。这比使用 isinstance(x, abc.Iterable) 更准确,因为 iter(x) 还考虑了传统的 __getitem__ 方法,而 Iterable ABC 则不考虑。

关于第 3 点:迭代只提供 __getitem__ 而不提供 __iter__ 的对象

BasicIterable 的实例进行迭代时,Python 会构造一个迭代器,尝试通过索引从零开始获取项目,直到引发 IndexError。演示对象的 __getitem__ 方法简单地返回作为参数传递给 __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

这里有另一个例子,以便更好地理解迭代器如何通过索引访问项目。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__。对于只提供 __getitem__ 的对象,iter 无法执行任何正常性检查,因为它无法检查对象的项目是否可以通过整数索引访问。

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

关于第 7 点:你的可迭代类应该实现 __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__ 的方式(使用方括号表示法)会很好地工作:

>>> 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__ 的特殊处理是出于向后兼容的原因。再次引用 Fluent Python

这就是为什么任何 Python 序列都是可迭代的:它们都实现了 __getitem__。实际上,标准序列也实现了 __iter__,而你的类也应该这样做,因为 __getitem__ 的特殊处理是出于向后兼容的原因,未来可能会被移除(尽管在我写这篇文章时并没有被弃用)。

712

鸭子类型

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

# for obj in iterator:
#     pass

类型检查

可以使用抽象基类。这些需要至少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)来检查,可以发现那些被注册为可迭代的类,或者有__iter__()方法的类,但它无法检测到那些使用__getitem__()方法进行迭代的类。判断一个对象是否可迭代的唯一可靠方法是调用iter(obj)

1056
  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 的词汇表中提到:

    Python 风格的编程方式是通过检查对象的方法或属性来判断对象的类型,而不是通过与某个类型对象的明确关系来判断(“如果它看起来像一只鸭子,并且叫声也像一只鸭子,那它肯定是一只鸭子。”)。这种方式强调接口而不是具体类型,使得设计良好的代码更灵活,允许多态替代。鸭子类型避免使用 type() 或 isinstance() 进行测试。相反,它通常采用 EAFP(更容易请求原谅而不是请求许可)风格的编程。

    ...

    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__ 方法可迭代的类。

撰写回答