Respuestas:
Los objetos iteradores en python se ajustan al protocolo iterador, lo que básicamente significa que proporcionan dos métodos: __iter__()
y __next__()
.
El __iter__
devuelve el objeto iterador y se llama implícitamente al comienzo de bucles.
El __next__()
método devuelve el siguiente valor y se llama implícitamente en cada incremento de bucle. Este método genera una excepción StopIteration cuando no hay más valor para devolver, que se captura implícitamente mediante construcciones en bucle para detener la iteración.
Aquí hay un ejemplo simple de un contador:
class Counter:
def __init__(self, low, high):
self.current = low - 1
self.high = high
def __iter__(self):
return self
def __next__(self): # Python 2: def next(self)
self.current += 1
if self.current < self.high:
return self.current
raise StopIteration
for c in Counter(3, 9):
print(c)
Esto imprimirá:
3
4
5
6
7
8
Esto es más fácil de escribir usando un generador, como se cubrió en una respuesta anterior:
def counter(low, high):
current = low
while current < high:
yield current
current += 1
for c in counter(3, 9):
print(c)
El resultado impreso será el mismo. Debajo del capó, el objeto generador admite el protocolo iterador y hace algo más o menos similar al contador de clase.
El artículo de David Mertz, Iterators and Simple Generators , es una muy buena introducción.
__next__
. counter
es un iterador, pero no es una secuencia. No almacena sus valores. No debería usar el contador en un bucle for doblemente anidado, por ejemplo.
__iter__
(además de in __init__
). De lo contrario, el objeto solo se puede iterar una vez. Por ejemplo, si dices ctr = Counters(3, 8)
, no puedes usar for c in ctr
más de una vez.
Counter
es un iterador, y se supone que los iteradores solo se repiten una vez. Si restablece self.current
en __iter__
, a continuación, un bucle anidado sobre el Counter
estaría completamente roto, y todo tipo de comportamientos asumidos de iteradores (que llamar iter
son violados en ellos es idempotente). Si desea poder iterar ctr
más de una vez, debe ser un iterador no iterador, donde devuelve un nuevo iterador cada vez que __iter__
se invoca. Intentar mezclar y combinar (un iterador que se restablece implícitamente cuando __iter__
se invoca) viola los protocolos.
Counter
fuera un iterador no iterador, eliminaría la definición de __next__
/ por next
completo, y probablemente la redefiniría __iter__
como una función generadora de la misma forma que el generador descrito al final de esta respuesta (excepto en lugar de los límites viniendo de argumentos a __iter__
, serían argumentos para __init__
guardar en self
y acceder desde self
adentro __iter__
).
Hay cuatro formas de construir una función iterativa:
__iter__
y__next__
(o next
en Python 2.x))__getitem__
)Ejemplos:
# generator
def uc_gen(text):
for char in text.upper():
yield char
# generator expression
def uc_genexp(text):
return (char for char in text.upper())
# iterator protocol
class uc_iter():
def __init__(self, text):
self.text = text.upper()
self.index = 0
def __iter__(self):
return self
def __next__(self):
try:
result = self.text[self.index]
except IndexError:
raise StopIteration
self.index += 1
return result
# getitem method
class uc_getitem():
def __init__(self, text):
self.text = text.upper()
def __getitem__(self, index):
return self.text[index]
Para ver los cuatro métodos en acción:
for iterator in uc_gen, uc_genexp, uc_iter, uc_getitem:
for ch in iterator('abcde'):
print(ch, end=' ')
print()
Lo que resulta en:
A B C D E
A B C D E
A B C D E
A B C D E
Nota :
Los dos tipos de generador ( uc_gen
y uc_genexp
) no pueden ser reversed()
; el iterador simple ( uc_iter
) necesitaría el __reversed__
método mágico (que, según los documentos , debe devolver un nuevo iterador, pero devolver self
trabajos (al menos en CPython)); y getitem iteratable ( uc_getitem
) debe tener el __len__
método mágico:
# for uc_iter we add __reversed__ and update __next__
def __reversed__(self):
self.index = -1
return self
def __next__(self):
try:
result = self.text[self.index]
except IndexError:
raise StopIteration
self.index += -1 if self.index < 0 else +1
return result
# for uc_getitem
def __len__(self)
return len(self.text)
Para responder la pregunta secundaria del Coronel Panic sobre un iterador infinitamente evaluado perezosamente, aquí están esos ejemplos, usando cada uno de los cuatro métodos anteriores:
# generator
def even_gen():
result = 0
while True:
yield result
result += 2
# generator expression
def even_genexp():
return (num for num in even_gen()) # or even_iter or even_getitem
# not much value under these circumstances
# iterator protocol
class even_iter():
def __init__(self):
self.value = 0
def __iter__(self):
return self
def __next__(self):
next_value = self.value
self.value += 2
return next_value
# getitem method
class even_getitem():
def __getitem__(self, index):
return index * 2
import random
for iterator in even_gen, even_genexp, even_iter, even_getitem:
limit = random.randint(15, 30)
count = 0
for even in iterator():
print even,
count += 1
if count >= limit:
break
print
Lo que resulta en (al menos para mi ejecución de muestra):
0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42 44 46 48 50 52 54
0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38
0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30
0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32
¿Cómo elegir cuál usar? Esto es principalmente una cuestión de gustos. Los dos métodos que veo con mayor frecuencia son los generadores y el protocolo iterador, así como un híbrido (que __iter__
devuelve un generador).
Las expresiones generadoras son útiles para reemplazar las comprensiones de listas (son perezosas y, por lo tanto, pueden ahorrar recursos).
Si necesita compatibilidad con versiones anteriores de Python 2.x, use __getitem__
.
uc_iter
debe caducar cuando se hace (de lo contrario, sería infinita); si desea hacerlo nuevamente, debe obtener un nuevo iterador llamando uc_iter()
nuevamente.
self.index = 0
en el __iter__
modo que se puede repetir muchas veces. De lo contrario no puedes.
En primer lugar, el módulo itertools es increíblemente útil para todo tipo de casos en los que un iterador sería útil, pero aquí está todo lo que necesita para crear un iterador en python:
rendimiento
¿No es genial? El rendimiento se puede usar para reemplazar un retorno normal en una función. Devuelve el objeto de la misma manera, pero en lugar de destruir el estado y salir, guarda el estado para cuando desea ejecutar la próxima iteración. Aquí hay un ejemplo de esto en acción extraído directamente de la lista de funciones de itertools :
def count(n=0):
while True:
yield n
n += 1
Como se indica en la descripción de las funciones (es la función count () del módulo itertools ...), produce un iterador que devuelve enteros consecutivos que comienzan con n.
Las expresiones generadoras son otra lata de gusanos (¡gusanos increíbles!). Se pueden usar en lugar de una Comprensión de lista para ahorrar memoria (las comprensiones de lista crean una lista en la memoria que se destruye después del uso si no se asigna a una variable, pero las expresiones generadoras pueden crear un Objeto generador ... que es una forma elegante de diciendo Iterator). Aquí hay un ejemplo de una definición de expresión de generador:
gen = (n for n in xrange(0,11))
Esto es muy similar a nuestra definición de iterador anterior, excepto que el rango completo está predeterminado entre 0 y 10.
Acabo de encontrar xrange () (sorprendido de que no lo haya visto antes ...) y lo agregué al ejemplo anterior. xrange () es una versión iterable de range () que tiene la ventaja de no construir previamente la lista. Sería muy útil si tuviera un corpus gigante de datos para iterar y solo tuviera tanta memoria para hacerlo.
Veo que algunos de ustedes haciendo return self
en __iter__
. Solo quería señalar que en __iter__
sí mismo puede ser un generador (eliminando así la necesidad __next__
y generando StopIteration
excepciones)
class range:
def __init__(self,a,b):
self.a = a
self.b = b
def __iter__(self):
i = self.a
while i < self.b:
yield i
i+=1
Por supuesto, aquí también se podría hacer un generador directamente, pero para clases más complejas puede ser útil.
return self
en __iter__
. Cuando intentaba usarlo yield
, encontré que su código hacía exactamente lo que quería probar.
next()
? return iter(self).next()
?
self.current
cualquier otro contador. ¡Esta debería ser la respuesta mejor votada!
iter
a instancias de la clase, pero no son en sí instancias de la clase.
Esta pregunta es sobre objetos iterables, no sobre iteradores. En Python, las secuencias también son iterables, por lo que una forma de hacer una clase iterable es hacer que se comporte como una secuencia, es decir, darle __getitem__
y __len__
métodos. He probado esto en Python 2 y 3.
class CustomRange:
def __init__(self, low, high):
self.low = low
self.high = high
def __getitem__(self, item):
if item >= len(self):
raise IndexError("CustomRange index out of range")
return self.low + item
def __len__(self):
return self.high - self.low
cr = CustomRange(0, 10)
for i in cr:
print(i)
__len__()
método. __getitem__
solo con el comportamiento esperado es suficiente.
Todas las respuestas en esta página son realmente geniales para un objeto complejo. Sin embargo, para los que contienen incorporado tipos iterables como atributos, como str
, list
, set
o dict
, o cualquier aplicación de collections.Iterable
, se puede omitir ciertas cosas en su clase.
class Test(object):
def __init__(self, string):
self.string = string
def __iter__(self):
# since your string is already iterable
return (ch for ch in self.string)
# or simply
return self.string.__iter__()
# also
return iter(self.string)
Se puede usar como:
for x in Test("abcde"):
print(x)
# prints
# a
# b
# c
# d
# e
return iter(self.string)
.
Esta es una función iterable sin yield
. Hace uso de la iter
función y un cierre que mantiene su estado en un mutable ( list
) en el ámbito de cierre para python 2.
def count(low, high):
counter = [0]
def tmp():
val = low + counter[0]
if val < high:
counter[0] += 1
return val
return None
return iter(tmp, None)
Para Python 3, el estado de cierre se mantiene inmutable en el ámbito de inclusión y nonlocal
se utiliza en el ámbito local para actualizar la variable de estado.
def count(low, high):
counter = 0
def tmp():
nonlocal counter
val = low + counter
if val < high:
counter += 1
return val
return None
return iter(tmp, None)
Prueba;
for i in count(1,10):
print(i)
1
2
3
4
5
6
7
8
9
iter
, pero para ser claros: esto es más complejo y menos eficiente que simplemente usar una yield
función de generador basada; Python tiene un montón de soporte de intérprete para yield
funciones de generador basadas que no puede aprovechar aquí, haciendo que este código sea significativamente más lento. Sin embargo, votó a favor.
Si buscas algo corto y simple, tal vez sea suficiente para ti:
class A(object):
def __init__(self, l):
self.data = l
def __iter__(self):
return iter(self.data)
ejemplo de uso:
In [3]: a = A([2,3,4])
In [4]: [i for i in a]
Out[4]: [2, 3, 4]
Inspirado por la respuesta de Matt Gregory aquí hay un iterador un poco más complicado que devolverá a, b, ..., z, aa, ab, ..., zz, aaa, aab, ..., zzy, zzz
class AlphaCounter:
def __init__(self, low, high):
self.current = low
self.high = high
def __iter__(self):
return self
def __next__(self): # Python 3: def __next__(self)
alpha = ' abcdefghijklmnopqrstuvwxyz'
n_current = sum([(alpha.find(self.current[x])* 26**(len(self.current)-x-1)) for x in range(len(self.current))])
n_high = sum([(alpha.find(self.high[x])* 26**(len(self.high)-x-1)) for x in range(len(self.high))])
if n_current > n_high:
raise StopIteration
else:
increment = True
ret = ''
for x in self.current[::-1]:
if 'z' == x:
if increment:
ret += 'a'
else:
ret += 'z'
else:
if increment:
ret += alpha[alpha.find(x)+1]
increment = False
else:
ret += x
if increment:
ret += 'a'
tmp = self.current
self.current = ret[::-1]
return tmp
for c in AlphaCounter('a', 'zzz'):
print(c)