Crear un iterador básico de Python


427

¿Cómo se podría crear una función iterativa (u objeto iterador) en python?

+3

Aquí hay dos preguntas, ambas importantes. ¿Cómo hacer que una clase sea iterable (es decir, con la que pueda pasar el bucle)? ¿Y cómo hacer una función que devuelve una secuencia con evaluación perezosa? 12 jul. 152015-07-12 13:26:31

+4

Un buen ejercicio, creo, es escribir una clase que represente los números pares (una secuencia infinita). 12 jul. 152015-07-12 13:30:02

+1

@ColonelPanic: De acuerdo, agregué el número infinito de ejemplos a [mi respuesta] (http://stackoverflow.com/a/7542261/208880). 04 feb. 162016-02-04 17:25:03

499

Los objetos Iterator en python se ajustan al protocolo del 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 inicio de los bucles. El método next() devuelve el siguiente valor y se invoca implícitamente en cada incremento de bucle. next() genera una excepción StopIteration cuando no hay más valor para devolver, que es implícitamente capturado por las construcciones de bucle para detener la iteración.

Aquí está un ejemplo sencillo de un contador:

class Counter: 
    def __init__(self, low, high): 
     self.current = low 
     self.high = high 

    def __iter__(self): 
     return self 

    def next(self): # Python 3: def __next__(self) 
     if self.current > self.high: 
      raise StopIteration 
     else: 
      self.current += 1 
      return self.current - 1 


for c in Counter(3, 8): 
    print c 

Esto imprimirá:

3 
4 
5 
6 
7 
8 

Esto es más fácil escribir utilizando un generador, como se explica en la respuesta anterior:

def counter(low, high): 
    current = low 
    while current <= high: 
     yield current 
     current += 1 

for c in counter(3, 8): 
    print c 

La salida impresa será la misma. Debajo del capó, el objeto del generador admite el protocolo del iterador y hace algo más o menos similar al contador de la clase.

El artículo de David Mertz, Iterators and Simple Generators, es una muy buena introducción.

+45

Tenga en cuenta que la función 'next()' no 'yield' values, '' return's them. 17 oct. 122012-10-17 07:03:44

+46

Esto no es válido en Python 3 --- tiene que ser '__next __()'. 23 sep. 132013-09-23 03:42:21

+2

Esto es sobre todo una buena respuesta, pero el hecho de que devuelve self es un poco subóptimo. Por ejemplo, si utilizó el mismo objeto contador en un bucle doble anidado, probablemente no obtendrá el comportamiento que usted quiso decir. 06 feb. 142014-02-06 23:33:31

+12

No, los iteradores DEBERÍAN regresar ellos mismos. Iterables devuelven iteradores, pero los iterables no deben implementar '__next__'. 'counter' es un iterador, pero no es una secuencia. No almacena sus valores. No deberías estar usando el contador en un bucle forzado anidado doblemente, por ejemplo. 21 feb. 142014-02-21 08:42:44

+3

En el ejemplo de contador, self.current debe asignarse en '__iter__' (además de en' __init__'). De lo contrario, el objeto puede repetirse solo una vez. Por ej., Si dice 'ctr = Contadores (3, 8)', entonces no puede usar 'for c in ctr' más de una vez. 05 abr. 162016-04-05 23:00:00

  0

¿no debería el código \ _ \ _ iter \ _ \ _ establecer el valor de self.current? 13 mar. 172017-03-13 02:01:54

  0

@Curt: Absolutamente no. 'Counter' es un iterador, y se supone que los iteradores deben repetirse una vez. Si restablece 'self.current' en' __iter__', entonces un bucle anidado sobre el 'Contador' se romperá completamente, y se violarán todos los tipos de comportamientos asumidos de los iteradores (que la llamada 'iter' en ellos es idempotente). Si desea poder iterar 'ctr' más de una vez, necesita ser iterable no iterador, donde devuelve un iterador completamente nuevo cada vez que se invoca' __iter__'. Intentar mezclar y combinar (un iterador que se restablece implícitamente cuando se invoca '__iter__') viola los protocolos. 24 feb. 182018-02-24 01:16:43

  0

Por ejemplo, si 'Counter' iba a ser iterable no iterador, eliminaría por completo la definición de' __next__'/'next', y probablemente redefina' __iter__' como una función del generador de la misma forma que el generador descrito al final de esta respuesta (excepto que en lugar de los límites que provienen de los argumentos a '__iter__', serían argumentos para' __init__' guardado en 'self' y accedido desde' self' en '__iter__'). 24 feb. 182018-02-24 01:19:21

  0

BTW, una cosa útil que hacer si quieres escribir clases de iterador portátiles es definir ya sea 'next' o' __next__', luego asignar un nombre al otro ('next = __next__' o' __next__ = next' dependiendo del nombre que dio el método). Tener ambos nombres definidos significa que funciona tanto en Py2 como en Py3 sin cambios en el código fuente. 24 feb. 182018-02-24 01:43:52


97

En primer lugar la itertools module es increíblemente útil para todo tipo de casos en los que un iterador sería útil, pero aquí es todo lo que necesita para crear un iterador en Python:

rendimiento

¿No es genial? El rendimiento se puede utilizar para reemplazar una normal return en una función. Devuelve el objeto exactamente igual, pero en lugar de destruir el estado y salir, guarda el estado para cuando quieras ejecutar la siguiente iteración. Aquí es un ejemplo de ello en la acción tiró directamente de la itertools function list:

def count(n=0): 
    while True: 
     yield n 
     n += 1 

Como se indica en la descripción funciones (es la count() función del módulo itertools ...), que produce un iterador que devuelve enteros consecutivos comenzando con n.

Generator expressions son otras latas de gusanos (¡gusanos increíbles!). Se pueden usar en lugar de List Comprehension para ahorrar memoria (las listas de comprensión crean una lista en la memoria que se destruye después del uso si no está asignada a una variable, pero las expresiones del generador pueden crear un Objeto generador ... que es una forma elegante de decir Iterador). Este es un ejemplo de una definición de la expresión del generador:

gen = (n for n in xrange(0,11)) 

Esto es muy similar a nuestra definición iterador anteriormente, excepto la gama completa está predeterminada a estar entre 0 y 10.

acabo de encontrar xrange() (me sorprendió no haberlo visto antes ...) y lo agregué al ejemplo anterior. xrange() es una versión iterativa de range() que tiene la ventaja de no preconstruir la lista. Sería muy útil si tuviera un corpus de datos gigantes para iterar y solo tuviera tanta memoria para hacerlo.

+17

a partir de Python 3.0 ya no existe un xrange() y el nuevo rango() se comporta como el antiguo xrange() 18 dic. 082008-12-18 17:30:08

+6

Aún debe usar xrange en 2._, porque 2to3 lo traduce automáticamente. 22 jul. 112011-07-22 18:03:40


305

Hay cuatro maneras de construir una función iterativa:

Ejemplos:

# generator 
def uc_gen(text): 
    for char in text: 
     yield char.upper() 

# generator expression 
def uc_genexp(text): 
    return (char.upper() for char in text) 

# iterator protocol 
class uc_iter(): 
    def __init__(self, text): 
     self.text = text 
     self.index = 0 
    def __iter__(self): 
     return self 
    def __next__(self): 
     try: 
      result = self.text[self.index].upper() 
     except IndexError: 
      raise StopIteration 
     self.index += 1 
     return result 

# getitem method 
class uc_getitem(): 
    def __init__(self, text): 
     self.text = text 
    def __getitem__(self, index): 
     result = self.text[index].upper() 
     return result 

para ver los cuatro métodos de acción:

for iterator in uc_gen, uc_genexp, uc_iter, uc_getitem: 
    for ch in iterator('abcde'): 
     print ch, 
    print 

que se traduce en:

A B C D E 
A B C D E 
A B C D E 
A B C D E 

Nota:

Los dos generador tipos (uc_gen y uc_genexp) no pueden ser reversed(); el iterador simple (uc_iter) necesitaría el método mágico __reversed__ (que debe devolver un nuevo iterador que vaya hacia atrás); y la GetItem iterables (uc_getitem) debe tener el método __len__ mágica:

# for uc_iter 
    def __reversed__(self): 
     return reversed(self.text) 

    # for uc_getitem 
    def __len__(self) 
     return len(self.text) 

Para responder a la pregunta secundaria del Coronel de pánico sobre un infinito iterador con pereza evaluado, aquí están los ejemplos, utilizando 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 

que se traduce en (al menos para mi análisis de la 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 
+4

Me gusta este resumen porque está completo. Esas tres formas (rendimiento, expresión del generador e iterador) son esencialmente las mismas, aunque algunas son más convenientes que otras. El operador de rendimiento captura la "continuación" que contiene el estado (por ejemplo, el índice que estamos haciendo). La información se guarda en el "cierre" de la continuación. El modo de iterador guarda la misma información dentro de los campos del iterador, que es esencialmente lo mismo que un cierre. El método __getitem__ es un poco diferente porque se indexa en los contenidos y no es de naturaleza iterativa. 05 jul. 132013-07-05 01:04:22

  0

No está incrementando el índice en su último enfoque, 'uc_getitem()'.En realidad, en la reflexión, no debería incrementar el índice, porque no lo está manteniendo. Pero tampoco es una forma de iteración abstracta. 05 nov. 132013-11-05 15:25:55

+2

@metaperl: En realidad, lo es. En los cuatro casos anteriores, puede usar el mismo código para iterar. 05 nov. 132013-11-05 16:37:21


79

Veo que algunos de ustedes están haciendo return self en __iter__. Sólo quería señalar que __iter__ sí mismo puede ser un generador (eliminando así la necesidad de __next__ y elevar 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 que aquí uno puede así realizar directamente un generador, pero para las clases más complejas que puede sé útil.

+5

¡Genial! Es tan aburrido escribir 'return self' en' __iter__'. Cuando iba a intentar usar 'yield', encontré tu código haciendo exactamente lo que quiero probar. 05 feb. 132013-02-05 19:32:35

+2

Pero en este caso, ¿cómo se implementaría 'next()'? 'return iter (self) .next()'? 05 abr. 132013-04-05 19:52:12

+4

@Lenna, ya está "implementado" porque iter (self) devuelve un iterador, no una instancia de rango. 07 abr. 132013-04-07 17:31:31

  0

@Manux 'iter (rango (5,10)). Next()' es un poco engorroso. Es cierto que es un mal ejemplo para el comportamiento 'siguiente'. Todavía estoy interesado en cómo darle a la instancia de rango un atributo 'next'. 24 abr. 132013-04-24 19:06:36

+2

Esta es la manera más fácil de hacerlo, y no implica tener que realizar un seguimiento de, p. '' self.current'' o cualquier otro contador. ¡Esta debería ser la respuesta mejor votada! 31 mar. 142014-03-31 13:35:53

  0

La diferencia: '__iter__' es un generador es un objeto diferente de la instancia' range() '. A veces esto importa, otras veces no. 09 nov. 142014-11-09 19:42:19

  0

No debe usar 'iter (rango (5,10)). Next()' de todos modos. La forma correcta es 'next (iter (rango (5,10)))'. El 'next' builtin está ahí exactamente, por lo que no tiene que importar si se devuelve' self' en esta situación. 14 mar. 162016-03-14 20:41:59

  0

votado - este método también funciona más como se espera (en relación con la respuesta aceptada) para algo como 'r = rango (5); list_of_lists = list ([ri, list (r)] para ri en r) ' 17 sep. 162016-09-17 16:09:23

  0

Es interesante que' __iter__' no tenga que levantar StopIteration. Un problema al definir solo '__iter__' es que' next (myiterator) 'no funciona si' __next__' no 'devuelve' elementos individuales. Necesitar usar 'next (iter (myiterator))' no es un sustituto sabio. 18 abr. 172017-04-18 14:42:52

  0

Para ser claros, este enfoque hace que su clase * iterable *, pero no * iterator *. Obtienes fresh * iterators * cada vez que llamas a 'iter' en instancias de la clase, pero no son instancias de la clase. 24 feb. 182018-02-24 01:25:07

  0

@MadPhysicist: en Python 2, 'iter (rango (5,10)). Next()' y 'next (iter (rango (5,10)))' ya son exactamente equivalentes. La ventaja de 'next' como función no tiene nada que ver con si' self' es devuelto por '__iter__' (el comportamiento es idéntico para ambos fragmentos de código). Las ventajas de la función incorporada 'siguiente' son: 1. Funciona igual en Py2 y Py3, aunque el método cambie los nombres entre ellos y 2. Cuando corresponda, se le puede dar un segundo argumento para devolver en el evento que el iterador ya está agotado, en lugar de subir 'StopIteration'. 24 feb. 182018-02-24 01:27:54


3

Esta es una función iterable sin yield. Se hace uso de la función iter y un cierre que mantiene su estado en un mutable (list) en el ámbito circundante para el pitón 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 en una inmutable en el ámbito circundante y nonlocal se usa 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 
  0

Siempre aprecio un uso inteligente de two-arg 'iter', pero solo para ser claro: esto es más complejo y menos eficiente que simplemente usar una función de generador basada en' yield'; Python tiene un montón de soporte de intérprete para las funciones del generador basadas en el rendimiento que no puede aprovechar aquí, lo que hace que este código sea mucho más lento. Up-votado no obstante. 24 feb. 182018-02-24 01:30:05


7

Esta pregunta se trata de objetos iterables, no de iteradores. En Python, las secuencias son iterables también, por lo que una forma de hacer una clase iterable es hacer que se comporte como una secuencia, es decir, darle los métodos __getitem__ y __len__. 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)