Construir un iterador básico de Python


Respuestas:


650

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.


44
En general, esta es una buena respuesta, pero el hecho de que se devuelva a sí mismo es un poco subóptimo. Por ejemplo, si usa el mismo objeto contador en un bucle doblemente anidado, probablemente no obtendrá el comportamiento que quería decir.
Casey Rodarmor

22
No, los iteradores DEBEN devolverse. Los iteradores devuelven iteradores, pero los iterables no deberían implementarse __next__. counteres 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.
leewz

44
En el ejemplo de Counter, self.current debe asignarse en __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 ctrmás de una vez.
Curt

77
@Curt: Absolutamente no. Counteres un iterador, y se supone que los iteradores solo se repiten una vez. Si restablece self.currenten __iter__, a continuación, un bucle anidado sobre el Counterestaría completamente roto, y todo tipo de comportamientos asumidos de iteradores (que llamar iterson violados en ellos es idempotente). Si desea poder iterar ctrmá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.
ShadowRanger

2
Por ejemplo, si Counterfuera un iterador no iterador, eliminaría la definición de __next__/ por nextcompleto, 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 selfy acceder desde selfadentro __iter__).
ShadowRanger

427

Hay cuatro formas de construir una función iterativa:

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_geny 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 selftrabajos (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__.


44
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. La forma 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

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

1
@Asterisk: No, una instancia de uc_iterdebe caducar cuando se hace (de lo contrario, sería infinita); si desea hacerlo nuevamente, debe obtener un nuevo iterador llamando uc_iter()nuevamente.
Ethan Furman

2
Se puede establecer self.index = 0en el __iter__modo que se puede repetir muchas veces. De lo contrario no puedes.
John Strood

1
Si pudiera perder el tiempo, agradecería una explicación de por qué elegiría cualquiera de los métodos sobre los demás.
aaaaaa

103

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.


20
a partir de python 3.0 ya no hay un xrange () y el nuevo rango () se comporta como el viejo xrange ()

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

100

Veo que algunos de ustedes haciendo return selfen __iter__. Solo quería señalar que en __iter__sí mismo puede ser un generador (eliminando así la necesidad __next__y generando StopIterationexcepciones)

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.


55
¡Excelente! Es tan aburrido escribir simplemente return selfen __iter__. Cuando intentaba usarlo yield, encontré que su código hacía exactamente lo que quería probar.
Rayo

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

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

3
Esta es la forma más fácil de hacerlo, y no implica tener que realizar un seguimiento, por ejemplo, de self.currentcualquier otro contador. ¡Esta debería ser la respuesta mejor votada!
astrofrog

44
Para ser claros, este enfoque hace que su clase sea iterable , pero no un iterador . Obtiene iteradores nuevos cada vez que llama itera instancias de la clase, pero no son en sí instancias de la clase.
ShadowRanger

13

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)

1
No tiene que tener un __len__()método. __getitem__solo con el comportamiento esperado es suficiente.
BlackJack

5

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, seto 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

1
Como usted ha dicho, la cadena es ya tan iterables qué la expresión generador extra entre en lugar de simplemente pidiendo a la cadena para el iterador (que la expresión generadora hace internamente): return iter(self.string).
BlackJack

@BlackJack De hecho tienes razón. No sé qué me convenció para escribir de esa manera. Quizás estaba tratando de evitar cualquier confusión en una respuesta que intentaba explicar el funcionamiento de la sintaxis de iterador en términos de más sintaxis de iterador.
John Strood

3

Esta es una función iterable sin yield. Hace uso de la iterfunció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 nonlocalse 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

Siempre aprecio el uso inteligente de dos argumentos iter, pero para ser claros: esto es más complejo y menos eficiente que simplemente usar una yieldfunción de generador basada; Python tiene un montón de soporte de intérprete para yieldfunciones de generador basadas que no puede aprovechar aquí, haciendo que este código sea significativamente más lento. Sin embargo, votó a favor.
ShadowRanger

2

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]

-1

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)
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.