Saltar al contenido

En Python, ¿cómo puedo determinar si un objeto es iterable?

Solución:

  1. Verificando para __iter__ funciona en tipos de secuencia, pero fallaría, por ejemplo, en cadenas en Python 2. También me gustaría saber la respuesta correcta, hasta entonces, aquí hay una posibilidad (que también funcionaría con cadenas):

    from __future__ import print_function
    
    try:
        some_object_iterator = iter(some_object)
    except TypeError as te:
        print(some_object, 'is not iterable')
    

    los iter controles incorporados para el __iter__ método o en el caso de cadenas el __getitem__ método.

  2. Otro enfoque pitónico general es asumir un iterable, luego fallar con gracia si no funciona en el objeto dado. El glosario de Python:

    Estilo de programación pitónico que determina el tipo de un objeto mediante la inspección de su método o firma de atributo en lugar de una relación explícita con algún tipo de objeto (“Si parece un Pato y grazna como un Pato, debe ser un Pato. “) Al enfatizar interfaces en lugar de tipos específicos, el código bien diseñado mejora su flexibilidad al permitir la sustitución polimórfica. Duck-typing evita las pruebas usando type () o isinstance (). En cambio, normalmente emplea el estilo de programación EAFP (Más fácil de pedir perdón que permiso).

    try:
       _ = (e for e in my_object)
    except TypeError:
       print my_object, 'is not iterable'
    
  3. los collections El módulo proporciona algunas clases base abstractas, que permiten preguntar a las clases o instancias si proporcionan una funcionalidad particular, por ejemplo:

    from collections.abc import Iterable
    
    if isinstance(e, Iterable):
        # e is iterable
    

    Sin embargo, esto no comprueba las clases que son iterables a través de __getitem__.

Tipeo de pato

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

# for obj in iterator:
#     pass

Comprobación de tipo

Utilice las clases base abstractas. Necesitan al menos Python 2.6 y funcionan solo para clases de nuevo estilo.

from collections.abc import Iterable   # import directly from collections for Python < 3.3

if isinstance(theElement, Iterable):
    # iterable
else:
    # not iterable

Sin embargo, iter() es un poco más confiable como se describe en la documentación:

Comprobación isinstance(obj, Iterable) detecta clases que están registradas como iterables o que tienen un __iter__() método, pero no detecta clases que iteran con el __getitem__()
método. La única forma confiable de determinar si un objeto es iterable es llamar iter(obj).

Me gustaría arrojar un poco más de luz sobre la interacción de iter, __iter__ y __getitem__ y lo que pasa detrás de las cortinas. Armado con ese conocimiento, podrá comprender por qué lo mejor que puede hacer es

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

Primero enumeraré los hechos y luego seguiré con un recordatorio rápido de lo que sucede cuando emplea un for bucle en Python, seguido de una discusión para ilustrar los hechos.

Hechos

  1. Puede obtener un iterador de cualquier objeto o llamando iter(o) si se cumple al menos una de las siguientes condiciones:

    a) o tiene un __iter__ método que devuelve un objeto iterador. Un iterador es cualquier objeto con un __iter__ y un __next__ (Python 2: next) método.

    B) o tiene un __getitem__ método.

  2. Buscando una instancia de Iterable o Sequenceo comprobando el atributo __iter__ no es suficiente.

  3. Si un objeto o solo implementa __getitem__, pero no __iter__, iter(o) construirá un iterador que intenta obtener elementos de o por índice entero, comenzando en el índice 0. El iterador capturará cualquier IndexError (pero no otros errores) que se genera y luego aumenta StopIteration sí mismo.

  4. En el sentido más general, no hay forma de verificar si el iterador devuelto por iter está cuerdo aparte de probarlo.

  5. Si un objeto o implementos __iter__, los iter La función se asegurará de que el objeto devuelto por __iter__ es un iterador. No hay verificación de cordura si un objeto solo se implementa __getitem__.

  6. __iter__ gana. Si un objeto o implementa ambos __iter__ y __getitem__, iter(o) llamará __iter__.

  7. Si desea que sus propios objetos sean iterables, implemente siempre el __iter__ método.

for bucles

Para seguir adelante, necesita comprender lo que sucede cuando emplea un for bucle en Python. No dude en pasar directamente a la siguiente sección si ya lo sabe.

Cuando usas for item in o para algún objeto iterable o, Python llama iter(o) y espera un objeto iterador como valor de retorno. Un iterador es cualquier objeto que implementa un __next__ (o next en Python 2) método y un __iter__ método.

Por convención, el __iter__ El método de un iterador debe devolver el objeto en sí (es decir, return self). Python luego llama next en el iterador hasta StopIteration es elevado. Todo esto sucede implícitamente, pero la siguiente demostración lo hace 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

Iteración sobre un 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

Discusión e ilustraciones

En el punto 1 y 2: obtener un iterador y controles poco confiables

Considere la siguiente clase:

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

Vocación iter con una instancia de BasicIterable devolverá un iterador sin ningún problema porque BasicIterable implementos __getitem__.

>>> b = BasicIterable()
>>> iter(b)
<iterator object at 0x7f1ab216e320>

Sin embargo, es importante tener en cuenta que b no tiene el __iter__ atributo y no se considera una instancia de Iterable o Sequence:

>>> from collections import Iterable, Sequence
>>> hasattr(b, '__iter__')
False
>>> isinstance(b, Iterable)
False
>>> isinstance(b, Sequence)
False

Es por eso que Fluent Python de Luciano Ramalho recomienda llamar iter y manejando el potencial TypeError como la forma más precisa de comprobar si un objeto es iterable. Citando directamente del libro:

A partir de Python 3.4, la forma más precisa de comprobar si un objeto x es iterable es llamar iter(x) y manejar un TypeError excepción si no lo es. Esto es más preciso que usar isinstance(x, abc.Iterable) , porque iter(x) también considera el legado __getitem__ método, mientras que el Iterable ABC no lo hace.

En el punto 3: iterar sobre objetos que solo proporcionan __getitem__, pero no __iter__

Iterando sobre una instancia de BasicIterable funciona como se esperaba: Python construye un iterador que intenta buscar elementos por índice, comenzando en cero, hasta que IndexError es elevado. El objeto de demostración __getitem__ El método simplemente devuelve el item que se suministró como argumento para __getitem__(self, item) por el iterador devuelto por 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

Tenga en cuenta que el iterador aumenta StopIteration cuando no puede devolver el siguiente artículo y que el IndexError que se cría para item == 3 se maneja internamente. Esta es la razón por la que recorrer un BasicIterable con un for bucle funciona como se esperaba:

>>> for x in b:
...     print(x)
...
0
1
2

Aquí hay otro ejemplo para llevar a casa el concepto de cómo el iterador regresó por iter intenta acceder a los elementos por índice. WrappedDict no hereda de dict, lo que significa que las instancias no tendrán __iter__ método.

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

Tenga en cuenta que las llamadas a __getitem__ son delegados a dict.__getitem__ para lo cual la notación de corchetes es simplemente una abreviatura.

>>> w = WrappedDict({-1: 'not printed',
...                   0: 'hi', 1: 'StackOverflow', 2: '!',
...                   4: 'not printed', 
...                   'x': 'not printed'})
>>> for x in w:
...     print(x)
... 
hi
StackOverflow
!

Sobre los puntos 4 y 5: iter busca un iterador cuando llama __iter__:

Cuando iter(o) se llama para un objeto o, iter se asegurará de que el valor de retorno de __iter__, si el método está presente, es un iterador. Esto significa que el objeto devuelto debe implementar __next__ (o next en Python 2) y __iter__. iter no puede realizar comprobaciones de cordura para objetos que solo proporcionan __getitem__, porque no tiene forma de verificar si los elementos del objeto son accesibles por índice entero.

class FailIterIterable(object):
    def __iter__(self):
        return object() # not an iterator

class FailGetitemIterable(object):
    def __getitem__(self, item):
        raise Exception

Tenga en cuenta que la construcción de un iterador a partir de FailIterIterable instancias falla inmediatamente, mientras se construye un iterador a partir de FailGetItemIterable tiene éxito, pero lanzará una excepción en la primera llamada a __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

Sobre el punto 6: __iter__ gana

Este es sencillo. Si un objeto implementa __iter__ y __getitem__, iter llamará __iter__. Considere la siguiente clase

class IterWinsDemo(object):
    def __iter__(self):
        return iter(['__iter__', 'wins'])

    def __getitem__(self, item):
        return ['__getitem__', 'wins'][item]

y la salida al recorrer una instancia:

>>> iwd = IterWinsDemo()
>>> for x in iwd:
...     print(x)
...
__iter__
wins

En el punto 7: sus clases iterables deberían implementar __iter__

Puede preguntarse por qué la mayoría de las secuencias integradas como list implementar un __iter__ método cuando __getitem__ sería suficiente.

class WrappedList(object): # note: no inheritance from list!
    def __init__(self, lst):
        self._list = lst

    def __getitem__(self, item):
        return self._list[item]

Después de todo, la iteración sobre instancias de la clase anterior, que delega llamadas a __getitem__ para list.__getitem__ (usando la notación de corchetes), funcionará bien:

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

Las razones por las que se deben implementar sus iterables personalizados __iter__ son como sigue:

  1. Si implementa __iter__, las instancias se considerarán iterables, y isinstance(o, collections.abc.Iterable) volverá True.
  2. Si el objeto devuelto por __iter__ no es un iterador, iter fallará inmediatamente y generará un TypeError.
  3. El manejo especial de __getitem__ existe por razones de compatibilidad con versiones anteriores. Citando nuevamente de Fluent Python:

Es por eso que cualquier secuencia de Python es iterable: todas implementan __getitem__ . De hecho, las secuencias estándar también implementan __iter__, y el tuyo también debería, porque el manejo especial de __getitem__ existe por razones de compatibilidad con versiones anteriores y puede desaparecer en el futuro (aunque no está desaprobado mientras escribo esto).

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *