Diferencia entre los generadores e iteradores de Python

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

iterator es un concepto más general: cualquier objeto cuya clase tenga un método next ( __next__ en Python 3) y un método __iter__ 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 expresiones de yield (declaraciones de yield , en Python 2.5 y anteriores), y es un objeto que cumple con la definición de un iterator del párrafo anterior.

Es posible que desee utilizar un iterador personalizado, en lugar de un generador, cuando necesite una clase con un comportamiento de mantenimiento del estado algo complejo, o desee exponer otros métodos además del next (y __iter__ y __init__ ). La mayoría de las veces, un generador (a veces, para necesidades suficientemente simples, una expresión del generador) es suficiente, y es más fácil 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 construirlo 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): 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 los Squares clase podrí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.

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

En resumen: los iteradores son objetos que tienen un __iter__ y __next__ (a next en Python 2). Los generadores proporcionan una manera fácil e integrada de crear instancias de iteradores.

Una función con rendimiento aún es 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 del generador también devuelve un generador:

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

Para una exposición más detallada 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 simple 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)  >>> a_generator = a_function() # when called >>> type(a_generator) # returns a 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 método __iter__ que devuelve un iterador:

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

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

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

Los iteradores requieren un método next o __next__

En Python 2:

 >>> collections.Iterator() Traceback (most recent call last): File "", line 1, in  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 "", line 1, in  TypeError: Can't instantiate abstract class Iterator with abstract methods __next__ 

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

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

Se __iter__ método __iter__ cuando intenta utilizar un objeto con un bucle for. Luego se __next__ método __next__ en el objeto iterador para sacar cada elemento del bucle. El iterador genera StopIteration cuando lo ha agotado y no puede reutilizarse en ese momento.

De la documentación

En 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 de iterador. Si el método __iter__() un objeto contenedor se implementa como un generador, devolverá automáticamente un objeto iterador (técnicamente, un objeto generador) que suministra los __iter__() y next() [ __next__() en Python 3]. Puede encontrar más información sobre los generadores en la documentación de la expresión de rendimiento.

(Énfasis añadido.)

Así que 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 tal vez más simple, una Expresión de Generador (funciona de manera similar a la lista de comprensión):

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

Todos ellos pueden ser utilizados 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 de Python como un objeto sobre el que se puede iterar.

Sin embargo, en la gran mayoría de los casos, es más adecuado utilizar el yield para definir una función que devuelva un iterador del generador o considere las expresiones del generador.

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

Iteradores

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

Generadores:

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

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

Cuando se llama a una función de generador, devuelve un objeto generador sin siquiera comenzar la ejecución de la función. Cuando se llama al método next() por primera vez, la función comienza a ejecutarse hasta que alcanza la statement de rendimiento que devuelve el valor obtenido. El rendimiento hace un seguimiento de es decir, recuerda la última ejecución. Y la segunda llamada next() 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 "", line 1, in  StopIteration >>> 

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

Las funciones del generador son funciones ordinarias definidas utilizando el yield lugar de return . Cuando se llama, una función de generador devuelve un objeto generador , que es un tipo de iterador: tiene un método next() . Cuando llama a next() , se devuelve el siguiente valor generado por la función del generador.

Se puede llamar a la función o al objeto “generador” según el documento de origen de Python que lea. El glosario de Python dice que las funciones del generador, mientras que la wiki de Python implica objetos generadores. El tutorial de Python notablemente logra 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 statement de rendimiento cuando quieren devolver datos. Cada vez que se llama a next () en él, el generador se reanuda donde lo dejó (recuerda todos los valores de datos y la última instrucción que se ejecutó).

Las dos primeras frases identifican generadores con funciones generadoras, mientras que la tercera frase los identifica con objetos generadores.

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

La expresión de rendimiento solo se usa cuando se define una función generadora, y solo se puede usar en el cuerpo de una definición de función. El uso de una expresión de rendimiento en una definición de función es suficiente para hacer que esa definición cree una función de generador en lugar de una función normal.

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

Entonces, en el uso formal y preciso, “generador” no calificado significa objeto generador, no función generadora.

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 … Generalmente se refiere a una función de generador, pero puede referirse a un iterador de generador en algunos contextos. En los casos en que el significado pretendido no está claro, el uso de los términos completos evita la ambigüedad.

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 declaraciones de yield . Las funciones de generador son una gran herramienta para crear objetos de iterador lo más fácil posible. La repetición del objeto Iterator por la función del generador también se denomina Objeto generador o Generador .

En este ejemplo, he creado una función Generador que devuelve un objeto Generador . Al igual que otros iteradores, los objetos del Generador se pueden usar en un bucle for 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)) # 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 

Así que una función de generador 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 el método __iter__ y __next__ (también denominado 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 da 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 

Todos tienen una respuesta muy agradable y detallada con ejemplos y realmente los aprecio. Solo quería dar una respuesta breve a algunas personas que aún no son muy claras conceptualmente:

Si crea su propio iterador, está un poco involucrado: tiene que crear una clase y al menos implementar el iter y los siguientes métodos. Pero qué pasa si no quiere pasar por esta molestia y quiere crear rápidamente un iterador. Afortunadamente, Python proporciona una forma abreviada de 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 método siguiente y usarlo en un bucle for). Este algo tiene un nombre en Python llamado Generador

Espero que se aclare un poco.

Las respuestas anteriores omitieron esta adición: un generador tiene un método close , mientras que los iteradores típicos no lo hacen. El método de close desencadena una excepción StopIteration en el generador, que puede quedar atrapada en una cláusula finally en ese iterador, para tener la oportunidad de ejecutar un poco de limpieza. Esta abstracción lo hace más útil en los iteradores grandes que simples. Uno puede cerrar un generador como se 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 tiene un método __iter__ solamente, los iteradores típicos tienen solo un método __next__ , los generadores tienen un __iter__ y un __next__ y un close adicional.

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 método close es una mayor capacidad de composición con yield from . Localmente, 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 puede detenerse antes de que llegue al final. Tiendo a ver los iteradores como un nivel primitivo de bajo nivel, excepto como literales.

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

Puedes 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 comprueba la huella de memoria, el generador necesita mucha menos memoria, ya que no necesita almacenar todos los valores en la memoria al mismo tiempo.