Diferencia entre los generadores e iteradores de Python

538

¿Cuál es la diferencia entre iteradores y generadores? Algunos ejemplos de cuándo usaría cada caso serían útiles.

newToProgramming
fuente

Respuestas:

544

iteratores un concepto más general: cualquier objeto cuya clase tiene un nextmétodo ( __next__en Python 3) y un __iter__método que sí return self.

Cada generador es un iterador, pero no al revés. Un generador se construye llamando a una función que tiene una o más yieldexpresiones ( yielddeclaraciones, en Python 2.5 y anteriores), y es un objeto que cumple con la definición de un párrafo anterior iterator.

Es posible que desee utilizar un iterador, en lugar de un generador, cuando se necesita una clase con un comportamiento un tanto complejo de mantener el estado, o si quiere exponer a otros métodos además next(y __iter__, y __init__). Muy a menudo, un generador (a veces, para necesidades suficientemente simples, una expresión de generador ) es suficiente, y es más simple de codificar porque el mantenimiento del estado (dentro de límites razonables) es básicamente "hecho por usted" cuando el marco se suspende y se reanuda.

Por ejemplo, un generador como:

def squares(start, stop):
    for i in range(start, stop):
        yield i * i

generator = squares(a, b)

o la expresión generadora equivalente (genexp)

generator = (i*i for i in range(a, b))

tomaría más código para construir como un iterador personalizado:

class Squares(object):
    def __init__(self, start, stop):
       self.start = start
       self.stop = stop
    def __iter__(self): return self
    def next(self): # __next__ in Python 3
       if self.start >= self.stop:
           raise StopIteration
       current = self.start * self.start
       self.start += 1
       return current

iterator = Squares(a, b)

Pero, por supuesto, con la clase Squarespodría ofrecer fácilmente métodos adicionales, es decir

    def current(self):
       return self.start

si tiene alguna necesidad real de dicha funcionalidad adicional en su aplicación.

Alex Martelli
fuente
¿Cómo uso el iterador, una vez que lo he creado?
Vincenzooo
@Vincenzooo que depende de lo que quieras hacer con él. O bien será parte de un for ... in ...:, pasado a una función, o iter.next()
llamarás
@Caleth Estaba preguntando acerca de la sintaxis exacta, porque recibí un error al intentar usar la for..insintaxis. Tal vez me faltaba algo, pero fue hace algún tiempo, no recuerdo si lo resolví. ¡Gracias!
Vincenzooo
137

¿Cuál es la diferencia entre iteradores y generadores? Algunos ejemplos de cuándo usaría cada caso serían útiles.

En resumen: los iteradores son objetos que tienen un método a __iter__y a __next__( nexten Python 2). Los generadores proporcionan una forma fácil e integrada de crear instancias de iteradores.

Una función con rendimiento sigue siendo una función que, cuando se llama, devuelve una instancia de un objeto generador:

def a_function():
    "when called, returns generator object"
    yield

Una expresión generadora también devuelve un generador:

a_generator = (i for i in range(0))

Para una exposición más profunda y ejemplos, sigue leyendo.

Un generador es un iterador

Específicamente, el generador es un subtipo de iterador.

>>> import collections, types
>>> issubclass(types.GeneratorType, collections.Iterator)
True

Podemos crear un generador de varias maneras. Una forma muy común y sencilla de hacerlo es con una función.

Específicamente, una función con rendimiento es una función que, cuando se llama, devuelve un generador:

>>> def a_function():
        "just a function definition with yield in it"
        yield
>>> type(a_function)
<class 'function'>
>>> a_generator = a_function()  # when called
>>> type(a_generator)           # returns a generator
<class 'generator'>

Y un generador, de nuevo, es un iterador:

>>> isinstance(a_generator, collections.Iterator)
True

Un iterador es un iterable

Un iterador es un Iterable,

>>> issubclass(collections.Iterator, collections.Iterable)
True

que requiere un __iter__método que devuelve un iterador:

>>> collections.Iterable()
Traceback (most recent call last):
  File "<pyshell#79>", line 1, in <module>
    collections.Iterable()
TypeError: Can't instantiate abstract class Iterable with abstract methods __iter__

Algunos ejemplos de iterables son las tuplas integradas, listas, diccionarios, conjuntos, conjuntos congelados, cadenas, cadenas de bytes, conjuntos de bytes, rangos y vistas de memoria:

>>> all(isinstance(element, collections.Iterable) for element in (
        (), [], {}, set(), frozenset(), '', b'', bytearray(), range(0), memoryview(b'')))
True

Los iteradores requieren un método nexto__next__

En Python 2:

>>> collections.Iterator()
Traceback (most recent call last):
  File "<pyshell#80>", line 1, in <module>
    collections.Iterator()
TypeError: Can't instantiate abstract class Iterator with abstract methods next

Y en Python 3:

>>> collections.Iterator()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class Iterator with abstract methods __next__

Podemos obtener los iteradores de los objetos integrados (u objetos personalizados) con la iterfunción:

>>> all(isinstance(iter(element), collections.Iterator) for element in (
        (), [], {}, set(), frozenset(), '', b'', bytearray(), range(0), memoryview(b'')))
True

Se __iter__llama al método cuando intenta utilizar un objeto con un ciclo for. Luego __next__se llama al método en el objeto iterador para obtener cada elemento para el bucle. El iterador se eleva StopIterationcuando lo ha agotado, y no puede reutilizarse en ese punto.

De la documentación

Desde la sección Tipos de generador de la sección Tipos de iterador de la documentación de Tipos incorporados :

Los generadores de Python proporcionan una forma conveniente de implementar el protocolo iterador. Si el __iter__()método de un objeto contenedor se implementa como un generador, devolverá automáticamente un objeto iterador (técnicamente, un objeto generador) que suministra los métodos __iter__()y next()[ __next__()en Python 3]. Se puede encontrar más información sobre generadores en la documentación de la expresión de rendimiento.

(Énfasis añadido.)

Entonces, de esto aprendemos que los generadores son un tipo (conveniente) de iterador.

Ejemplos de objetos iteradores

Puede crear un objeto que implemente el protocolo Iterator creando o extendiendo su propio objeto.

class Yes(collections.Iterator):

    def __init__(self, stop):
        self.x = 0
        self.stop = stop

    def __iter__(self):
        return self

    def next(self):
        if self.x < self.stop:
            self.x += 1
            return 'yes'
        else:
            # Iterators must raise when done, else considered broken
            raise StopIteration

    __next__ = next # Python 3 compatibility

Pero es más fácil simplemente usar un generador para hacer esto:

def yes(stop):
    for _ in range(stop):
        yield 'yes'

O quizás más simple, una Expresión de generador (funciona de manera similar a las comprensiones de listas):

yes_expr = ('yes' for _ in range(stop))

Todos se pueden usar de la misma manera:

>>> stop = 4             
>>> for i, y1, y2, y3 in zip(range(stop), Yes(stop), yes(stop), 
                             ('yes' for _ in range(stop))):
...     print('{0}: {1} == {2} == {3}'.format(i, y1, y2, y3))
...     
0: yes == yes == yes
1: yes == yes == yes
2: yes == yes == yes
3: yes == yes == yes

Conclusión

Puede usar el protocolo Iterator directamente cuando necesite extender un objeto Python como un objeto sobre el que se puede iterar.

Sin embargo, en la gran mayoría de los casos, es mejor usarlo yieldpara definir una función que devuelve un iterador generador o considerar expresiones de generador.

Finalmente, tenga en cuenta que los generadores proporcionan aún más funcionalidad como corutinas. Explico los Generadores, junto con la yielddeclaración, en profundidad sobre mi respuesta a "¿Qué hace la palabra clave" rendimiento "?".

Aaron Hall
fuente
41

Iteradores:

Los iteradores son objetos que utilizan el next()método para obtener el siguiente valor de secuencia.

Generadores:

Un generador es una función que produce o produce una secuencia de valores utilizando el yieldmétodo.

Cada next()llamada al método en el objeto generador (por ejemplo: fcomo en el ejemplo a continuación) devuelto por la función del generador (por ejemplo: foo()función en el ejemplo a continuación), genera el siguiente valor en secuencia.

Cuando se llama a una función generadora, devuelve un objeto generador sin siquiera comenzar la ejecución de la función. Cuando next()se llama al método por primera vez, la función comienza a ejecutarse hasta que alcanza la declaración de rendimiento que devuelve el valor obtenido. El rendimiento realiza un seguimiento, es decir, recuerda la última ejecución. Y la segunda next()llamada continúa desde el valor anterior.

El siguiente ejemplo demuestra la interacción entre el rendimiento y la llamada al siguiente método en el objeto generador.

>>> def foo():
...     print "begin"
...     for i in range(3):
...         print "before yield", i
...         yield i
...         print "after yield", i
...     print "end"
...
>>> f = foo()
>>> f.next()
begin
before yield 0            # Control is in for loop
0
>>> f.next()
after yield 0             
before yield 1            # Continue for loop
1
>>> f.next()
after yield 1
before yield 2
2
>>> f.next()
after yield 2
end
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>>

fuente
3
Para su información el rendimiento no es el método, que es la palabra clave
Jay Parikh
25

Agregar una respuesta porque ninguna de las respuestas existentes aborda específicamente la confusión en la literatura oficial.

Las funciones generadoras son funciones ordinarias definidas usando enyieldlugar dereturn. Cuando se llama, una función generadora devuelve un objeto generador , que es un tipo de iterador, tiene unnext()método. Cuando llamanext(), se devuelve el siguiente valor producido por la función del generador.

La función o el objeto pueden llamarse "generador" dependiendo del documento fuente de Python que lea. El glosario de Python dice funciones generadoras, mientras que la wiki de Python implica objetos generadores. El tutorial de Python logra notablemente implicar ambos usos en el espacio de tres oraciones:

Los generadores son una herramienta simple y poderosa para crear iteradores. Se escriben como funciones regulares, pero usan la declaración de rendimiento cuando quieren devolver datos. Cada vez que se llama a next (), el generador reanuda donde lo dejó (recuerda todos los valores de datos y qué declaración se ejecutó por última vez).

Las primeras dos oraciones identifican generadores con funciones generadoras, mientras que la tercera oración los identifica con objetos generadores.

A pesar de toda esta confusión, uno puede buscar la referencia del lenguaje Python para la palabra clara y final:

La expresión de rendimiento solo se usa al definir una función generadora, y solo se puede usar en el cuerpo de una definición de función. Usar una expresión de rendimiento en una definición de función es suficiente para hacer que esa definición cree una función generadora en lugar de una función normal.

Cuando se llama a una función generadora, devuelve un iterador conocido como generador. Ese generador controla la ejecución de una función generadora.

Entonces, en un uso formal y preciso, "generador" no calificado significa objeto generador, no función generador.

Las referencias anteriores son para Python 2 pero la referencia del lenguaje Python 3 dice lo mismo. Sin embargo, el glosario de Python 3 establece que

generador ... Por lo general, se refiere a una función generadora, pero puede referirse a un iterador generador en algunos contextos. En los casos en que el significado deseado no está claro, el uso de los términos completos evita la ambigüedad.

Pablo
fuente
No creo que haya mucha confusión entre las funciones del generador y los objetos del generador, por la misma razón generalmente no hay confusión entre las clases y sus instancias. En ambos casos, llama a uno para obtener el otro, y en una conversación informal (o documentación escrita rápidamente) puede usar el nombre de la clase o la palabra "generador" para cualquiera de ellos. Solo necesita ser explícito acerca de la "función generadora" versus el "objeto generador" en situaciones raras en las que está hablando.
Blckknght
66
1. Independientemente de las razones teóricas de por qué no debería haber confusión, los comentarios sobre otras respuestas a esta pregunta se niegan y contradicen entre sí sin resolución, lo que indica que existe una confusión real. 2. La imprecisión casual está bien, pero una fuente precisa y autorizada debería ser al menos una de las opciones en SO. Utilizo mucho las funciones y los objetos del generador en mi proyecto actual, y la distinción es muy importante al diseñar y codificar. Es bueno saber qué terminología usar ahora, así que no tengo que cambiar docenas de nombres de variables y comentarios más adelante.
Paul
2
Imagine una literatura matemática donde no se hace distinción entre una función y su valor de retorno. En ocasiones es conveniente combinarlos informalmente, pero aumenta el riesgo de una variedad de errores. Las matemáticas modernas avanzadas se verían obstaculizadas de manera significativa e innecesaria si la distinción no se formalizara en convención, lenguaje y notación.
Paul
2
Las funciones de orden superior que pasan por los generadores o las funciones del generador pueden sonar raras, pero para mí han estado surgiendo. Estoy trabajando en Apache Spark y aplica un estilo de programación muy funcional. Las funciones tienen que crear, pasar y pasar todo tipo de objetos para hacer las cosas. He tenido varias situaciones en las que perdí la noción del tipo de "generador" con el que estaba trabajando. Las sugerencias en nombres de variables y comentarios, usando la terminología consistente y correcta, ayudaron a aclarar la confusión. ¡La oscuridad de un pitonista puede ser el centro del diseño del proyecto de otro!
Paul
1
@ Paul, gracias por escribir esta respuesta. Esta confusión es importante porque la diferencia entre un objeto generador y una función generadora es la diferencia entre obtener el comportamiento deseado y tener que buscar generadores.
blujay
15

Todos tienen una respuesta realmente agradable y detallada con ejemplos y realmente lo aprecio. Solo quería dar unas pocas líneas de respuesta para las personas que aún no tienen una idea conceptual clara:

Si crea su propio iterador, es un poco complicado: debe crear una clase y al menos implementar el iter y los siguientes métodos. Pero, ¿qué pasa si no quieres pasar por esta molestia y quieres crear rápidamente un iterador? Afortunadamente, Python proporciona una forma abreviada para definir un iterador. Todo lo que necesita hacer es definir una función con al menos 1 llamada para ceder y ahora, cuando llame a esa función, devolverá " algo " que actuará como un iterador (puede llamar al siguiente método y usarlo en un bucle for). Este algo tiene un nombre en Python llamado Generador

Espero que eso aclare un poco.

Heapify
fuente
10

Las respuestas anteriores omitieron esta adición: un generador tiene un closemétodo, mientras que los iteradores típicos no. El closemétodo desencadena una StopIterationexcepción en el generador, que puede quedar atrapado en una finallycláusula en ese iterador, para tener la oportunidad de ejecutar una limpieza. Esta abstracción lo hace más utilizable en iteradores grandes que simples. Uno puede cerrar un generador como podría cerrar un archivo, sin tener que preocuparse por lo que hay debajo.

Dicho esto, mi respuesta personal a la primera pregunta sería: iteratable solo tiene un __iter__método, los iteradores típicos solo tienen un __next__método, los generadores tienen tanto an __iter__como ay __next__adicional close.

Para la segunda pregunta, mi respuesta personal sería: en una interfaz pública, tiendo a favorecer mucho a los generadores, ya que es más resistente: el closemétodo es más fácil de componer yield from. A nivel local, puedo usar iteradores, pero solo si es una estructura plana y simple (los iteradores no se componen fácilmente) y si hay razones para creer que la secuencia es bastante corta, especialmente si se puede detener antes de llegar al final. Tiendo a ver los iteradores como una primitiva de bajo nivel, excepto como literales.

Para el flujo de control, los generadores son un concepto tan importante como las promesas: ambos son abstractos y componibles.

Hibou57
fuente
¿Podría dar un ejemplo para ilustrar a qué se refiere cuando habla de composición? Además, ¿puede explicar lo que tiene en mente cuando habla de " iteradores típicos "?
bli
1
Otra respuesta ( stackoverflow.com/a/28353158/1878788 ) establece que "un iterador es un iterable". Dado que un iterable tiene un __iter__método, ¿cómo es que un iterador __next__solo puede tenerlo ? Si se supone que son iterables, esperaría que necesariamente lo tengan __iter__también.
bli
1
@bli: AFAICS esta respuesta aquí se refiere al estándar PEP234 , por lo que es correcto, mientras que la otra respuesta se refiere a alguna implementación, por lo que es cuestionable. El estándar solo requiere un __iter__en iterables para devolver un iterador, que solo requiere un nextmétodo ( __next__en Python3). No confunda los estándares (para escribir en un pato) con su implementación (cómo lo implementó un intérprete de Python en particular). Esto es un poco como la confusión entre las funciones del generador (definición) y los objetos del generador (implementación). ;)
Tino
7

Función de generador, objeto generador, generador:

Una función de generador es como una función normal en Python, pero contiene una o más yielddeclaraciones. Las funciones de generador son una gran herramienta para crear objetos Iterator lo más fácil posible. El objeto iterador returend por función de generador también se denomina objeto generador o generador .

En este ejemplo, he creado una función Generador que devuelve un objeto Generador <generator object fib at 0x01342480>. Al igual que otros iteradores, los objetos Generator pueden usarse en un forbucle o con la función incorporada next()que devuelve el siguiente valor del generador.

def fib(max):
    a, b = 0, 1
    for i in range(max):
        yield a
        a, b = b, a + b
print(fib(10))             #<generator object fib at 0x01342480>

for i in fib(10):
    print(i)               # 0 1 1 2 3 5 8 13 21 34


print(next(myfib))         #0
print(next(myfib))         #1
print(next(myfib))         #1
print(next(myfib))         #2

Entonces, una función generadora es la forma más fácil de crear un objeto Iterator.

Iterador :

Cada objeto generador es un iterador pero no al revés. Se puede crear un objeto iterador personalizado si su clase implementa __iter__y __next__método (también llamado protocolo iterador).

Sin embargo, es mucho más fácil usar la función de generadores para crear iteradores porque simplifican su creación, pero un Iterador personalizado le brinda más libertad y también puede implementar otros métodos de acuerdo con sus requisitos, como se muestra en el siguiente ejemplo.

class Fib:
    def __init__(self,max):
        self.current=0
        self.next=1
        self.max=max
        self.count=0

    def __iter__(self):
        return self

    def __next__(self):
        if self.count>self.max:
            raise StopIteration
        else:
            self.current,self.next=self.next,(self.current+self.next)
            self.count+=1
            return self.next-self.current

    def __str__(self):
        return "Generator object"

itobj=Fib(4)
print(itobj)               #Generator object

for i in Fib(4):  
    print(i)               #0 1 1 2

print(next(itobj))         #0
print(next(itobj))         #1
print(next(itobj))         #1
N Randhawa
fuente
6

Ejemplos de Ned Batchelder muy recomendados para iteradores y generadores.

Un método sin generadores que hace algo a los números pares

def evens(stream):
   them = []
   for n in stream:
      if n % 2 == 0:
         them.append(n)
   return them

mientras usando un generador

def evens(stream):
    for n in stream:
        if n % 2 == 0:
            yield n
  • No necesitamos ninguna lista ni una returndeclaración
  • Eficiente para flujo de longitud grande / infinita ... simplemente camina y produce el valor

Llamar al evensmétodo (generador) es como siempre

num = [...]
for n in evens(num):
   do_smth(n)
  • El generador también solía romper el doble bucle

Iterador

Un libro lleno de páginas es iterable , un marcador es un iterador

y este marcador no tiene nada que hacer excepto moverse next

litr = iter([1,2,3])
next(litr) ## 1
next(litr) ## 2
next(litr) ## 3
next(litr) ## StopIteration  (Exception) as we got end of the iterator

Para usar Generator ... necesitamos una función

Para usar Iterator ... necesitamos nextyiter

Como se ha dicho:

Una función de generador devuelve un objeto iterador

Todo el beneficio de Iterator:

Almacene un elemento a la vez en la memoria

Marwan Mostafa
fuente
Acerca de su primer fragmento de código, me gustaría saber qué más podría ser 'stream' arg que la lista []?
Iqra
5

Puede comparar ambos enfoques para los mismos datos:

def myGeneratorList(n):
    for i in range(n):
        yield i

def myIterableList(n):
    ll = n*[None]
    for i in range(n):
        ll[i] = i
    return ll

# Same values
ll1 = myGeneratorList(10)
ll2 = myIterableList(10)
for i1, i2 in zip(ll1, ll2):
    print("{} {}".format(i1, i2))

# Generator can only be read once
ll1 = myGeneratorList(10)
ll2 = myIterableList(10)

print("{} {}".format(len(list(ll1)), len(ll2)))
print("{} {}".format(len(list(ll1)), len(ll2)))

# Generator can be read several times if converted into iterable
ll1 = list(myGeneratorList(10))
ll2 = myIterableList(10)

print("{} {}".format(len(list(ll1)), len(ll2)))
print("{} {}".format(len(list(ll1)), len(ll2)))

Además, si verifica la huella de la memoria, el generador toma mucha menos memoria ya que no necesita almacenar todos los valores en la memoria al mismo tiempo.

tashuhka
fuente
1

Estoy escribiendo específicamente para los novatos de Python de una manera muy simple, aunque en el fondo Python hace muchas cosas.

Comencemos con lo muy básico:

Considera una lista,

l = [1,2,3]

Escribamos una función equivalente:

def f():
    return [1,2,3]

o / p de print(l): [1,2,3]& o / p deprint(f()) : [1,2,3]

Hagamos que la lista sea iterable: en Python, la lista siempre es iterable, lo que significa que puede aplicar el iterador cuando lo desee.

Apliquemos el iterador en la lista:

iter_l = iter(l) # iterator applied explicitly

Hagamos que una función sea iterable, es decir, escriba una función generadora equivalente. En python tan pronto como introduzca la palabra clave yield; se convierte en una función generadora y el iterador se aplicará implícitamente.

Nota: Cada generador siempre es iterable con el iterador implícito aplicado y aquí el iterador implícito es el punto crucial. Por lo tanto, la función del generador será:

def f():
  yield 1 
  yield 2
  yield 3

iter_f = f() # which is iter(f) as iterator is already applied implicitly

Entonces, si has observado, tan pronto como hiciste la función fa generador, ya es iter (f)

Ahora,

l es la lista, después de aplicar el método iterador "iter" se convierte en iter (l)

f ya es iter (f), después de aplicar el método iterador "iter" se convierte en iter (iter (f)), que nuevamente es iter (f)

Es como si estuvieras convirtiendo int a int (x) que ya es int y seguirá siendo int (x).

Por ejemplo o / p de:

print(type(iter(iter(l))))

es

<class 'list_iterator'>

Nunca olvides que esto es Python y no C o C ++

Por lo tanto, la conclusión de la explicación anterior es:

lista l ~ = iter (l)

función de generador f == iter (f)

Jyo the Whiff
fuente