Solución:
-
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. -
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'
-
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 llamariter(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
-
Puede obtener un iterador de cualquier objeto
o
llamandoiter(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. -
Buscando una instancia de
Iterable
oSequence
o comprobando el atributo__iter__
no es suficiente. -
Si un objeto
o
solo implementa__getitem__
, pero no__iter__
,iter(o)
construirá un iterador que intenta obtener elementos deo
por índice entero, comenzando en el índice 0. El iterador capturará cualquierIndexError
(pero no otros errores) que se genera y luego aumentaStopIteration
sí mismo. -
En el sentido más general, no hay forma de verificar si el iterador devuelto por
iter
está cuerdo aparte de probarlo. -
Si un objeto
o
implementos__iter__
, lositer
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__
. -
__iter__
gana. Si un objetoo
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 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 llamariter(x)
y manejar unTypeError
excepción si no lo es. Esto es más preciso que usarisinstance(x, abc.Iterable)
, porqueiter(x)
también considera el legado__getitem__
método, mientras que elIterable
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:
- Si implementa
__iter__
, las instancias se considerarán iterables, yisinstance(o, collections.abc.Iterable)
volveráTrue
. - Si el objeto devuelto por
__iter__
no es un iterador,iter
fallará inmediatamente y generará unTypeError
. - 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).