funcion - Diferencia entre los generadores e iteradores de Python
python funcion yield (8)
¿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__
( 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)
<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 método __iter__
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, 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 "<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 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 usar un objeto con un bucle for. Luego, se llama al 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 documentation Tipos incorporados:
Los generadores de Python proporcionan una forma conveniente de implementar el protocolo 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__()
ynext()
[__next__()
en Python 3]. Se 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 declaración de yield
, en mi respuesta a "¿Qué hace la palabra clave" rendimiento "?".
¿Cuál es la diferencia entre iteradores y generadores? Algunos ejemplos de cuándo usarías cada caso serían útiles.
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 de objetos del iterador 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 <generator object fib at 0x01342480>
. 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)) #<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
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
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 declaración 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 del 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 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.
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 de 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 declaración de rendimiento que devuelve el valor producido. 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 "<stdin>", line 1, in <module>
StopIteration
>>>
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 realizar una 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 cuestiones 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.
Todo el mundo tiene una respuesta muy agradable y detallada con ejemplos y realmente lo 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 quieres pasar por esta molestia y quieres 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.
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.