Construye un iterador Python básico

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

Los objetos de iterador 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 __iter__ implícitamente al comienzo 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 se captura implícitamente mediante construcciones de bucle para detener la iteración.

Aquí hay un ejemplo simple 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 de escribir usando un generador, como se explica en una 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. Bajo el capó, el objeto generador es compatible con el protocolo del iterador y hace algo más o menos similar a la clase Contador.

El artículo de David Mertz, Iteradores y generadores simples , es una introducción bastante buena.

Hay cuatro formas de construir una función iterativa:

  • crear un generador (utiliza la palabra clave de rendimiento )
  • usar una expresión generadora ( genexp )
  • crear un iterador (define __iter__ y __next__ (o next en Python 2.x))
  • crear una clase que Python pueda iterar por su cuenta ( define __getitem__ )

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 en acción:

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

Lo que resulta en:

 ABCDE ABCDE ABCDE ABCDE 

Nota :

Los dos tipos de generador ( uc_gen y uc_genexp ) no se pueden reversed() ; el iterador simple ( uc_iter ) necesitaría el método mágico __reversed__ (que debe devolver un nuevo iterador que va hacia atrás); y el getitem iteratable ( uc_getitem ) debe tener el método mágico __len__ :

  # 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 Panic sobre un iterador infinitamente evaluado perezosamente, aquí están esos 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 

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 

En primer lugar, el módulo de 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 utilizar para reemplazar un rendimiento 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 siguiente iteración. Este es un ejemplo de ello 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 funciones (es la función count () del módulo itertools …), produce un iterador que devuelve números enteros consecutivos que comienzan con n.

Las expresiones del generador son otra lata de gusanos (¡gusanos asombrosos!). 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 de su uso si no se asigna a una variable, pero las expresiones del generador pueden crear un Objeto generador … que es una forma elegante de diciendo iterador). 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 () (sorprendió que no lo había visto antes …) y lo agregué al ejemplo anterior. xrange () es una versión iterable de range () que tiene la ventaja de no pre-construir la lista. Sería muy útil si tuviera un gigantesco cuerpo de datos para iterar y solo tuviera tanta memoria para hacerlo.

Veo que algunos de ustedes se return self en __iter__ . Solo quería señalar que __iter__ sí mismo puede ser un generador (eliminando así la necesidad de __next__ y generando excepciones de StopIteration )

 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 puede hacer un generador directamente, pero para clases más complejas puede ser útil.

Esta pregunta es sobre objetos iterables, no sobre 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) 

Esta es una función iterable sin yield . Hace uso de la función iter y un cierre que mantiene su estado mutable ( list ) en el ámbito de aplicación de 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 de forma inmutable en el ámbito de cierre 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 

Todas las respuestas en esta página son realmente geniales para un objeto complejo. Pero para aquellos que contienen tipos iterables incorporados como atributos, como str , list , set o dict , o cualquier implementación de collections.Iterable . collections.Iterable , 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 string) 

Se puede utilizar como:

 for x in Test("abcde"): print(x) # prints # a # b # c # d # e 

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)