Me gustaría arrojar un poco más de luz sobre la interacción de iter, __iter__y __getitem__y lo que sucede 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 haré un seguimiento con un recordatorio rápido de lo que sucede cuando empleas un forbucle en Python, seguido de una discusión para ilustrar los hechos.
Hechos
Puede obtener un iterador de cualquier objeto ollamando iter(o)si al menos una de las siguientes condiciones es verdadera:
a) otiene un __iter__método que devuelve un objeto iterador. Un iterador es cualquier objeto con un método __iter__y un __next__(Python 2 next:).
b) otiene un __getitem__método.
Verificar una instancia de Iterableo Sequence, o verificar el atributo __iter__no es suficiente.
Si un objeto osolo se implementa __getitem__, pero no __iter__, iter(o)construirá un iterador que intente buscar elementos opor índice entero, comenzando en el índice 0. El iterador detectará cualquier IndexError(pero ningún otro error) que se genere y luego se levante StopIteration.
En el sentido más general, no hay forma de verificar si el iterador devuelto por iteres sensato que no sea probarlo.
Si se oimplementa un objeto __iter__, la iterfunción se asegurará de que el objeto devuelto por __iter__sea un iterador. No hay comprobación de cordura si un objeto solo se implementa __getitem__.
__iter__gana. Si un objeto oimplementa ambos __iter__y __getitem__, iter(o)llamará __iter__.
Si desea que sus propios objetos sean iterables, implemente siempre el __iter__método.
for bucles
Para seguirlo, necesita comprender qué sucede cuando emplea un forbucle en Python. Si ya lo sabe, puede pasar directamente a la siguiente sección.
Cuando se usa for item in opara 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 método __next__(o nexten Python 2) y un __iter__método.
Por convención, el __iter__método de un iterador debe devolver el objeto en sí (es decir return self). Python luego llama nextal iterador hasta que StopIterationse eleva. 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 los puntos 1 y 2: obtener un iterador y verificaciones poco confiables
Considere la siguiente clase:
class BasicIterable(object):
def __getitem__(self, item):
if item == 3:
raise IndexError
return item
Llamar itercon una instancia de BasicIterabledevolverá un iterador sin ningún problema porque se BasicIterableimplementa __getitem__.
>>> b = BasicIterable()
>>> iter(b)
<iterator object at 0x7f1ab216e320>
Sin embargo, es importante tener en cuenta que bno tiene el __iter__atributo y no se considera una instancia de Iterableo 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 itery manejar el potencial TypeErrorcomo la forma más precisa de verificar si un objeto es iterable. Citando directamente del libro:
A partir de Python 3.4, la forma más precisa de verificar si un objeto xes iterable es llamar iter(x)y manejar una TypeErrorexcepción si no lo es. Esto es más preciso que usarlo isinstance(x, abc.Iterable), porque iter(x)también considera el __getitem__método heredado , mientras que el IterableABC no.
En el punto 3: iterar sobre objetos que solo proporcionan __getitem__, pero no__iter__
Iterando sobre una instancia de BasicIterabletrabajos como se esperaba: Python construye un iterador que intenta buscar elementos por índice, comenzando en cero, hasta que IndexErrorse genera un. El __getitem__método del objeto de demostración simplemente devuelve el itemque fue suministrado como argumento __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 se eleva StopIterationcuando no puede devolver el siguiente elemento y que el IndexErrorque se genera item == 3se maneja internamente. Esta es la razón por la cual recorrer un a BasicIterablecon un forbucle 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ó al iterintentar acceder a los elementos por índice. WrappedDictno hereda de dict, lo que significa que las instancias no tendrán un __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__se delegan dict.__getitem__para las cuales 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
!
En los puntos 4 y 5: iterbusca un iterador cuando llama__iter__ :
Cuando iter(o)se llama a un objeto o, iterse asegurará de que el valor de retorno de __iter__, si el método está presente, sea un iterador. Esto significa que el objeto devuelto debe implementar __next__(o nexten Python 2) y __iter__. iterno puede realizar ninguna comprobación de cordura para los 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 FailIterIterableinstancias falla inmediatamente, mientras que la construcción de un iterador se realiza FailGetItemIterablecorrectamente, pero arrojará 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
En el punto 6: __iter__gana
Este es sencillo. Si un objeto implementa __iter__y __getitem__, iterllamará __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 listimplementar un __iter__método __getitem__, serían suficientes.
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 las instancias de la clase anterior, que delega llamadas __getitem__a list.__getitem__(usando la notación de corchetes), funcionará bien:
>>> wl = WrappedList(['A', 'B', 'C'])
>>> for x in wl:
... print(x)
...
A
B
C
Los motivos por los que deberían implementarse sus iterables personalizados __iter__son los siguientes:
- Si implementa
__iter__, las instancias se considerarán iterables y isinstance(o, collections.abc.Iterable)volverán True.
- Si el objeto devuelto por
__iter__no es un iterador, iterfallará inmediatamente y elevará a TypeError.
- 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 se implementan __getitem__. De hecho, las secuencias estándar también se implementan __iter__, y las suyas también deberían hacerlo, porque el manejo especial de __getitem__existe por razones de compatibilidad con versiones anteriores y puede desaparecer en el futuro (aunque no está en desuso mientras escribo esto).
__getitem__también es suficiente para hacer que un objeto sea iterable