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 for
bucle en Python, seguido de una discusión para ilustrar los hechos.
Hechos
Puede obtener un iterador de cualquier objeto o
llamando iter(o)
si al menos una de las siguientes condiciones es verdadera:
a) o
tiene 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) o
tiene un __getitem__
método.
Verificar una instancia de Iterable
o Sequence
, o verificar el atributo __iter__
no es suficiente.
Si un objeto o
solo se implementa __getitem__
, pero no __iter__
, iter(o)
construirá un iterador que intente buscar elementos o
por í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 iter
es sensato que no sea probarlo.
Si se o
implementa un objeto __iter__
, la iter
funció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 o
implementa 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 for
bucle en Python. Si ya lo sabe, puede pasar directamente a la siguiente sección.
Cuando se usa 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 método __next__
(o next
en 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 next
al iterador hasta que StopIteration
se 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 iter
con una instancia de BasicIterable
devolverá un iterador sin ningún problema porque se BasicIterable
implementa __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 manejar el potencial TypeError
como 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 x
es iterable es llamar iter(x)
y manejar una TypeError
excepció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 Iterable
ABC no.
En el punto 3: iterar sobre objetos que solo proporcionan __getitem__
, pero no__iter__
Iterando sobre una instancia de BasicIterable
trabajos como se esperaba: Python construye un iterador que intenta buscar elementos por índice, comenzando en cero, hasta que IndexError
se genera un. El __getitem__
método del objeto de demostración simplemente devuelve el item
que 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 StopIteration
cuando no puede devolver el siguiente elemento y que el IndexError
que se genera item == 3
se maneja internamente. Esta es la razón por la cual recorrer un a 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ó al iter
intentar acceder a los elementos por índice. WrappedDict
no 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: iter
busca un iterador cuando llama__iter__
:
Cuando iter(o)
se llama a un objeto o
, iter
se 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 next
en Python 2) y __iter__
. iter
no 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 FailIterIterable
instancias falla inmediatamente, mientras que la construcción de un iterador se realiza FailGetItemIterable
correctamente, 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__
, 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 __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, iter
fallará 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