在Python中,如何判断一个对象是否可迭代?
有没有像 isiterable
这样的办法?我目前找到的唯一解决方案是调用
hasattr(myObj, '__iter__')
但是我不确定这个方法是否可靠。
25 个回答
我想更详细地解释一下 iter
、__iter__
和 __getitem__
之间的关系,以及它们背后发生的事情。了解这些知识后,你就能明白为什么你能做到的最好是:
try:
iter(maybe_iterable)
print('iteration will probably work')
except TypeError:
print('not iterable')
我会先列出一些事实,然后简单回顾一下在 Python 中使用 for
循环时发生的事情,最后通过讨论来说明这些事实。
事实
你可以通过调用
iter(o)
从任何对象o
获取一个迭代器,只要满足以下条件之一:
a)o
有一个__iter__
方法,并返回一个迭代器对象。迭代器是任何具有__iter__
和__next__
(在 Python 2 中是next
)方法的对象。
b)o
有一个__getitem__
方法。检查一个对象是否是
Iterable
或Sequence
,或者检查__iter__
属性是不够的。如果一个对象
o
只实现了__getitem__
,而没有__iter__
,那么调用iter(o)
会构造一个迭代器,它会尝试通过整数索引从o
中获取项目,从索引 0 开始。这个迭代器会捕获任何引发的IndexError
(但不会捕获其他错误),然后自己引发StopIteration
。从最一般的意义上说,除了尝试使用迭代器外,没有办法检查
iter
返回的迭代器是否正常。如果一个对象
o
实现了__iter__
,那么iter
函数会确保__iter__
返回的对象是一个迭代器。如果一个对象只实现了__getitem__
,则没有任何正常性检查。__iter__
优先。如果一个对象o
同时实现了__iter__
和__getitem__
,那么iter(o)
会调用__iter__
。如果你想让自己的对象可迭代,务必要实现
__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__
属性,并不被认为是 Iterable
或 Sequence
的实例:
>>> 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__
的原因如下:
- 如果你实现了
__iter__
,实例将被视为可迭代对象,isinstance(o, collections.abc.Iterable)
将返回True
。 - 如果
__iter__
返回的对象不是迭代器,iter
将立即失败并引发TypeError
。 - 对
__getitem__
的特殊处理是出于向后兼容的原因。再次引用Fluent Python
:
这就是为什么任何 Python 序列都是可迭代的:它们都实现了
__getitem__
。实际上,标准序列也实现了__iter__
,而你的类也应该这样做,因为__getitem__
的特殊处理是出于向后兼容的原因,未来可能会被移除(尽管在我写这篇文章时并没有被弃用)。
鸭子类型
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)
。
检查一个对象是否有
__iter__
方法在序列类型上是有效的,但在Python 2中,这在字符串上就会失败。我也想知道正确的答案,直到那时,这里有一个可能的解决办法(也适用于字符串):try: some_object_iterator = iter(some_object) except TypeError as te: print(some_object, 'is not iterable')
内置的
iter
函数会检查对象是否有__iter__
方法,或者在字符串的情况下,它会检查__getitem__
方法。另一种通用的 Python 风格是先假设一个对象是可迭代的,如果不行再优雅地处理错误。Python 的词汇表中提到:
Python 风格的编程方式是通过检查对象的方法或属性来判断对象的类型,而不是通过与某个类型对象的明确关系来判断(“如果它看起来像一只鸭子,并且叫声也像一只鸭子,那它肯定是一只鸭子。”)。这种方式强调接口而不是具体类型,使得设计良好的代码更灵活,允许多态替代。鸭子类型避免使用 type() 或 isinstance() 进行测试。相反,它通常采用 EAFP(更容易请求原谅而不是请求许可)风格的编程。
...
try: _ = (e for e in my_object) except TypeError: print(my_object, 'is not iterable')
collections
模块提供了一些抽象基类,可以用来询问类或实例是否提供特定的功能,例如:from collections.abc import Iterable if isinstance(e, Iterable): # e is iterable
不过,这并不检查通过
__getitem__
方法可迭代的类。