В 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__
метода.Другой общий подход pythonic заключается в предположении, что объект итеративен, а затем изящно завершается ошибкой, если он не работает с данным объектом. Глоссарий Python.:
Стиль программирования Pythonic, который определяет тип объекта путем проверки сигнатуры его метода или атрибута, а не по явной связи с объектом некоторого типа ("Если он выглядит как утка и крякает как утка, это должна быть утка".) Делая упор на интерфейсы, а не на конкретные типы, хорошо продуманный код повышает свою гибкость, позволяя полиморфную замену. Использование скрытого ввода позволяет избежать тестов с использованием 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__
.
Ответ 2
Утиный ввод
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)
.
Ответ 3
Я хотел бы пролить немного больше света на взаимодействие iter
, __iter__
и __getitem__
и на то, что происходит за кулисами. Вооружившись этими знаниями, вы сможете понять, почему лучшее, что вы можете сделать, это
try:
iter(maybe_iterable)
print('iteration will probably work')
except TypeError:
print('not iterable')
Сначала я перечислю факты, а затем приведу краткое напоминание о том, что происходит, когда вы используете for
цикл в python, за которым последует обсуждение для иллюстрации фактов.
Факты
Вы можете получить итератор от любого объекта
o
путем вызоваiter(o)
, если выполняется хотя бы одно из следующих условий:
a)o
имеет__iter__
метод, который возвращает объект-итератор. Итератор - это любой объект с__iter__
и__next__
(Python 2:next
) методом.
b)o
имеет__getitem__
метод.Проверки экземпляра
Iterable
orSequence
или атрибута__iter__
недостаточно.Если объект
o
реализует только__getitem__
, но не__iter__
,iter(o)
создаст итератор, который пытается извлекать элементы изo
по целочисленному индексу, начиная с индекса 0. Итератор перехватит любуюIndexError
(но не другие ошибки), которая возникает, а затем вызываетStopIteration
себя.In the most general sense, there's no way to check whether the iterator returned by
iter
is sane other than to try it out.If an object
o
implements__iter__
, theiter
function will make sure
that the object returned by__iter__
is an iterator. There is no sanity check
if an object only implements__getitem__
.__iter__
wins. If an objecto
implements both__iter__
and__getitem__
,iter(o)
will call__iter__
.If you want to make your own objects iterable, always implement the
__iter__
method.
for
loops
In order to follow along, you need an understanding of what happens when you employ a for
loop in Python. Feel free to skip right to the next section if you already know.
When you use for item in o
for some iterable object o
, Python calls iter(o)
and expects an iterator object as the return value. An iterator is any object which implements a __next__
(or next
in Python 2) method and an __iter__
method.
By convention, the __iter__
method of an iterator should return the object itself (i.e. return self
). Python then calls next
on the iterator until StopIteration
is raised. All of this happens implicitly, but the following demonstration makes it visible:
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
Iteration over a 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
Discussion and illustrations
On point 1 and 2: getting an iterator and unreliable checks
Consider the following class:
class BasicIterable(object):
def __getitem__(self, item):
if item == 3:
raise IndexError
return item
Calling iter
with an instance of BasicIterable
will return an iterator without any problems because BasicIterable
implements __getitem__
.
>>> b = BasicIterable()
>>> iter(b)
<iterator object at 0x7f1ab216e320>
However, it is important to note that b
does not have the __iter__
attribute and is not considered an instance of Iterable
or Sequence
:
>>> from collections import Iterable, Sequence
>>> hasattr(b, '__iter__')
False
>>> isinstance(b, Iterable)
False
>>> isinstance(b, Sequence)
False
This is why Fluent Python by Luciano Ramalho recommends calling iter
and handling the potential TypeError
as the most accurate way to check whether an object is iterable. Quoting directly from the book:
As of Python 3.4, the most accurate way to check whether an object
x
is iterable is to calliter(x)
and handle aTypeError
exception if it isn’t. This is more accurate than usingisinstance(x, abc.Iterable)
, becauseiter(x)
also considers the legacy__getitem__
method, while theIterable
ABC does not.
On point 3: Iterating over objects which only provide __getitem__
, but not __iter__
Iterating over an instance of BasicIterable
works as expected: Python
constructs an iterator that tries to fetch items by index, starting at zero, until an IndexError
is raised. The demo object's __getitem__
method simply returns the item
which was supplied as the argument to __getitem__(self, item)
by the iterator returned by 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
Note that the iterator raises StopIteration
when it cannot return the next item and that the IndexError
which is raised for item == 3
is handled internally. This is why looping over a BasicIterable
with a for
loop works as expected:
>>> for x in b:
... print(x)
...
0
1
2
Here's another example in order to drive home the concept of how the iterator returned by iter
tries to access items by index. WrappedDict
does not inherit from dict
, which means instances won't have an __iter__
method.
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__
:
Когда iter(o)
вызывается для объекта o
, iter
убедитесь, что возвращаемое значение __iter__
, если метод присутствует, является итератором. Это означает , что возвращаемый объект должен реализовывать __next__
(или next
в Python 2) и __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
По пункту 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__
to list.__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__
существует по соображениям обратной совместимости и может исчезнуть в будущем (хотя она не устарела, когда я пишу это).
Ответ 4
В последнее время я довольно много изучал эту проблему. Исходя из этого, я пришел к выводу, что на сегодняшний день это лучший подход:
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'
Если бы мы использовали insinstance()
, мы бы случайно не посчитали экземпляры Faker (или любые другие объекты, имеющие только __getitem__
) итеративными:
>>> from collections.abc import Iterable
>>> from faker import Faker
>>> isinstance(Faker(), Iterable)
False
В предыдущих ответах отмечалось, что использование iter()
безопаснее, поскольку старый способ реализации итерации в Python был основан на __getitem__
и isinstance()
подход не обнаружил бы этого. Возможно, это было верно для старых версий Python, но, основываясь на моем довольно исчерпывающем тестировании, isinstance()
отлично работает в наши дни. Единственный случай, когда isinstance()
не сработало, но iter()
сработало, был с UserDict
при использовании Python 2. Если это актуально, можно использовать isinstance(item, (Iterable, UserDict))
для решения этой проблемы.