2008-08-21 43 views
427

¿Cómo se podría crear una función iterativa (u objeto iterador) en python?Crear un iterador básico de 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? –

+4

Un buen ejercicio, creo, es escribir una clase que represente los números pares (una secuencia infinita). –

+1

@ColonelPanic: De acuerdo, agregué el número infinito de ejemplos a [mi respuesta] (http://stackoverflow.com/a/7542261/208880). –

Respuesta

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. –

+46

Esto no es válido en Python 3 --- tiene que ser '__next __()'. – Aerovistae

+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. –

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() – hop

+6

Aún debe usar xrange en 2._, porque 2to3 lo traduce automáticamente. – Phob

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. – Ian

+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. –

+2

@metaperl: En realidad, lo es. En los cuatro casos anteriores, puede usar el mismo código para iterar. –

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. – Ray

+2

Pero en este caso, ¿cómo se implementaría 'next()'? 'return iter (self) .next()'? – Lenna

+4

@Lenna, ya está "implementado" porque iter (self) devuelve un iterador, no una instancia de rango. – Manux

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. – ShadowRanger

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)