В Python, как мне определить, является ли объект итеративным? - PullRequest
929 голосов
/ 23 декабря 2009

Есть ли такой метод, как isiterable? Единственное решение, которое я нашел, это позвонить

hasattr(myObj, '__iter__')

Но я не уверен, насколько это глупо.

Ответы [ 20 ]

757 голосов
/ 23 декабря 2009
  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:

    Стиль программирования Pythonic, который определяет тип объекта путем проверки его метода или сигнатуры атрибута, а не путем явной связи с каким-либо объектом типа («Если он выглядит как утка и крякает как утка , это должна быть утка . ") Уделяя особое внимание интерфейсам, а не конкретным типам, хорошо разработанный код повышает его гибкость, позволяя полиморфное замещение. Утиная типография избегает тестов с использованием 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__.

532 голосов
/ 23 декабря 2009

Утка набрав

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).

88 голосов
/ 04 апреля 2016

Я хотел бы пролить немного больше света на взаимодействие iter, __iter__ и __getitem__ и то, что происходит за кулисами. Вооружившись этими знаниями, вы сможете понять, почему лучшее, что вы можете сделать, это

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

Сначала я перечислю факты, а затем кратко напомню, что происходит, когда вы используете цикл for в python, после чего следует обсуждение, чтобы проиллюстрировать факты.

Факты

  1. Вы можете получить итератор из любого объекта o, вызвав iter(o), если выполняется хотя бы одно из следующих условий:

    a) o имеет метод __iter__ который возвращает объект итератора. Итератор - это любой объект с __iter__ и __next__ (Python 2: next) методом.

    b) o имеет метод __getitem__.

  2. Проверка экземпляра Iterable или Sequence или проверка наличия атрибута __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 петли

Чтобы следовать, вам нужно понять, что происходит, когда вы используете цикл for в Python. Не стесняйтесь переходить к следующему разделу, если вы уже знаете.

Когда вы используете for item in o для некоторого итерируемого объекта o, Python вызывает iter(o) и ожидает объект итератора в качестве возвращаемого значения. Итератор - это любой объект, который реализует метод __next__ (или next в Python 2) и метод __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

Вот почему Fluent Python от Luciano Ramalho рекомендует вызывать 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, когда он не может вернуть следующий элемент, и что IndexError, который вызывается для item == 3, обрабатывается внутренне. Вот почему цикл по BasicIterable с циклом for работает, как и ожидалось:

>>> 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__ (или 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__ до list.__getitem__ (используя квадратную скобку), будет работать нормально:

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

Причины, по которым ваши пользовательские итерации должны реализовывать __iter__, заключаются в следующем:

  1. Если вы реализуете __iter__, экземпляры будут считаться итеративными, а isinstance(o, collections.Iterable) вернет True.
  2. Если объект, возвращаемый __iter__, не является итератором, iter немедленно завершится ошибкой и вызовет TypeError.
  3. Специальная обработка __getitem__ существует в целях обратной совместимости. Цитирую снова из Fluent Python:

Именно поэтому любая последовательность Python является итеративной: все они реализуют __getitem__. По факту, стандартные последовательности также реализуют __iter__, и ваш тоже должен, потому что специальная обработка __getitem__ существует по причинам обратной совместимости и может быть ушел в будущее (хотя это не устарело, когда я пишу это).

31 голосов
/ 23 декабря 2009

Этого недостаточно: объект, возвращаемый __iter__, должен реализовывать протокол итерации (т. Е. Метод next). См. Соответствующий раздел в документации .

В Python хорошей практикой является "попробовать и посмотреть" вместо "проверки".

20 голосов
/ 23 декабря 2009

В Python <= 2.5 вы не можете и не должны - итеративный был "неформальный" интерфейс. </p>

Но начиная с Python 2.6 и 3.0 вы можете использовать новую инфраструктуру ABC (абстрактный базовый класс) вместе с некоторыми встроенными ABC, которые доступны в модуле коллекций:

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 довольно полезной.

20 голосов
/ 23 декабря 2009
try:
  #treat object as iterable
except TypeError, e:
  #object is not actually iterable

Не запускайте проверки, чтобы увидеть , является ли ваша утка действительно уткой , чтобы увидеть, является ли она итеративной или нет, относитесь к ней, как если бы она была, и жалуйтесь, если это не так.

18 голосов
/ 09 ноября 2010

Лучшее решение, которое я нашел до сих пор:

hasattr(obj, '__contains__')

, который в основном проверяет, реализует ли объект оператор in.

Преимущества (ни одно из других решений не имеет всех трех):

  • это выражение (работает как лямбда , в отличие от try ... кроме варианта)
  • это (должно быть) реализовано всеми итерациями, включая строки (в отличие от __iter__)
  • работает на любом Python> = 2.5

Примечания:

  • философия Python «просить прощения, а не разрешения» не работает, например, когда в списке есть как итерируемые, так и не итерируемые, и вам нужно обрабатывать каждый элемент по-разному в соответствии с его типом (обработка итерируемых при попытке и не перебираемых, за исключением того, что будет работать, но это будет выглядеть уродливо и вводит в заблуждение)
  • решения этой проблемы, которые пытаются фактически выполнить итерацию по объекту (например, [x для x в obj]), чтобы проверить, является ли он итеративным, может привести к значительным потерям производительности для больших итерируемых элементов (особенно, если вам просто нужны первые несколько элементов итерируемый, например) и его следует избегать
13 голосов
/ 23 декабря 2009

Я нашел хорошее решение здесь :

isiterable = lambda obj: isinstance(obj, basestring) \
    or getattr(obj, '__iter__', False)
13 голосов
/ 23 декабря 2009

Вы можете попробовать это:

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

Если мы можем создать генератор, который выполняет итерации по нему (но никогда не использовать генератор, чтобы он не занимал место), это можно повторить. Похоже, что-то вроде "дух". Почему вы должны определить, является ли переменная итеративной в первую очередь?

11 голосов
/ 16 февраля 2018

Начиная с Python 3.5 вы можете использовать модуль typing из стандартной библиотеки для вещей, связанных с типом:

from typing import Iterable

...

if isinstance(my_item, Iterable):
    print(True)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...