协慌网

登录 贡献 社区

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

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

hasattr(myObj, '__iter__')

但我不确定这是多么万无一失。

答案

  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. 另一种通用的 pythonic 方法是假设一个可迭代的,然后如果它不能在给定的对象上工作则优雅地失败。 Python 词汇表:

    Pythonic 编程风格通过检查其方法或属性签名而不是通过与某个类型对象的显式关系来确定对象的类型(“如果它看起来像鸭子和嘎嘎叫鸭子 ,它必须是鸭子 。”)通过强调接口而不是特定类型,精心设计的代码通过允许多态替换来提高其灵活性。 Duck-typing 避免使用 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__迭代的类。

鸭打字

try:
    iterator = iter(theElement)
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(theElement, Iterable):
    # iterable
else:
    # not iterable

但是,如文档所述, iter()更可靠:

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

我想更多地了解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使用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

使用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 的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__方法简单地返回item将其作为供给该参数__getitem__(self, item)通过返回的迭代器iter

>>> 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将调用__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__getitem__足够时实现__iter__方法。

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

自定义 iterables 应该实现__iter__的原因如下:

  1. 如果实现__iter__ ,则实例将被视为 iterables,而isinstance(o, collections.Iterable)将返回True
  2. 如果__iter__返回的对象不是迭代器,则iter将立即失败并引发TypeError
  3. 出于向后兼容性原因,存在__getitem__的特殊处理。再次从 Fluent Python 引用:

这就是为什么任何 Python 序列都是可迭代的:它们都实现了__getitem__ 。事实上,标准序列也实现__iter__ ,你也应该实现,因为__getitem__的特殊处理是出于向后兼容的原因而存在,并且将来可能会消失(虽然我写这篇文章时不会弃用)。