Para entender lo que yield
hace, debes entender qué son los generadores . Y antes de que pueda comprender los generadores, debe comprender los iterables .
Iterables
Cuando crea una lista, puede leer sus elementos uno por uno. Leer sus elementos uno por uno se llama iteración:
>>> mylist = [1, 2, 3]
>>> for i in mylist:
... print(i)
1
2
3
mylist
Es un iterable . Cuando usas una comprensión de lista, creas una lista, y así un iterable:
>>> mylist = [x*x for x in range(3)]
>>> for i in mylist:
... print(i)
0
1
4
Todo lo que puedes usar " for... in...
" es iterable; lists
, strings
archivos ...
Estos iterables son útiles porque puede leerlos todo lo que desee, pero almacena todos los valores en la memoria y esto no siempre es lo que desea cuando tiene muchos valores.
Generadores
Los generadores son iteradores, un tipo de iterativo que solo puede iterar una vez . Los generadores no almacenan todos los valores en la memoria, generan los valores sobre la marcha :
>>> mygenerator = (x*x for x in range(3))
>>> for i in mygenerator:
... print(i)
0
1
4
Es igual, excepto que usaste en ()
lugar de []
. PERO, no puede realizar for i in mygenerator
una segunda vez ya que los generadores solo se pueden usar una vez: calculan 0, luego se olvidan y calculan 1, y terminan de calcular 4, uno por uno.
rendimiento
yield
es una palabra clave que se usa como return
, excepto que la función devolverá un generador.
>>> def createGenerator():
... mylist = range(3)
... for i in mylist:
... yield i*i
...
>>> mygenerator = createGenerator() # create a generator
>>> print(mygenerator) # mygenerator is an object!
<generator object createGenerator at 0xb7555c34>
>>> for i in mygenerator:
... print(i)
0
1
4
Aquí es un ejemplo inútil, pero es útil cuando sabes que tu función devolverá un gran conjunto de valores que solo necesitarás leer una vez.
Para dominar yield
, debe comprender que cuando llama a la función, el código que ha escrito en el cuerpo de la función no se ejecuta. La función solo devuelve el objeto generador, esto es un poco complicado :-)
Luego, su código continuará desde donde lo dejó cada vez que for
use el generador.
Ahora la parte difícil:
La primera vez que for
llama al objeto generador creado a partir de su función, ejecutará el código en su función desde el principio hasta que llegue yield
, luego devolverá el primer valor del bucle. Luego, cada llamada posterior ejecutará otra iteración del bucle que ha escrito en la función y devolverá el siguiente valor. Esto continuará hasta que el generador se considere vacío, lo que sucede cuando la función se ejecuta sin presionar yield
. Eso puede ser porque el ciclo ha llegado a su fin, o porque ya no satisfaces un "if/else"
.
Tu código explicado
Generador:
# Here you create the method of the node object that will return the generator
def _get_child_candidates(self, distance, min_dist, max_dist):
# Here is the code that will be called each time you use the generator object:
# If there is still a child of the node object on its left
# AND if the distance is ok, return the next child
if self._leftchild and distance - max_dist < self._median:
yield self._leftchild
# If there is still a child of the node object on its right
# AND if the distance is ok, return the next child
if self._rightchild and distance + max_dist >= self._median:
yield self._rightchild
# If the function arrives here, the generator will be considered empty
# there is no more than two values: the left and the right children
Llamador:
# Create an empty list and a list with the current object reference
result, candidates = list(), [self]
# Loop on candidates (they contain only one element at the beginning)
while candidates:
# Get the last candidate and remove it from the list
node = candidates.pop()
# Get the distance between obj and the candidate
distance = node._get_dist(obj)
# If distance is ok, then you can fill the result
if distance <= max_dist and distance >= min_dist:
result.extend(node._values)
# Add the children of the candidate in the candidate's list
# so the loop will keep running until it will have looked
# at all the children of the children of the children, etc. of the candidate
candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
return result
Este código contiene varias partes inteligentes:
El ciclo itera en una lista, pero la lista se expande mientras se repite el ciclo :-) Es una forma concisa de revisar todos estos datos anidados, incluso si es un poco peligroso, ya que puede terminar con un ciclo infinito. En este caso, candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
agote todos los valores del generador, pero while
sigue creando nuevos objetos generadores que producirán valores diferentes de los anteriores ya que no se aplica en el mismo nodo.
El extend()
método es un método de objeto de lista que espera un iterable y agrega sus valores a la lista.
Por lo general, le pasamos una lista:
>>> a = [1, 2]
>>> b = [3, 4]
>>> a.extend(b)
>>> print(a)
[1, 2, 3, 4]
Pero en su código, obtiene un generador, lo cual es bueno porque:
- No necesita leer los valores dos veces.
- Es posible que tenga muchos hijos y no quiera que todos estén almacenados en la memoria.
Y funciona porque a Python no le importa si el argumento de un método es una lista o no. ¡Python espera iterables para que funcione con cadenas, listas, tuplas y generadores! Esto se llama escribir pato y es una de las razones por las que Python es tan genial. Pero esta es otra historia, para otra pregunta ...
Puede detenerse aquí o leer un poco para ver un uso avanzado de un generador:
Controlando el agotamiento de un generador
>>> class Bank(): # Let's create a bank, building ATMs
... crisis = False
... def create_atm(self):
... while not self.crisis:
... yield "$100"
>>> hsbc = Bank() # When everything's ok the ATM gives you as much as you want
>>> corner_street_atm = hsbc.create_atm()
>>> print(corner_street_atm.next())
$100
>>> print(corner_street_atm.next())
$100
>>> print([corner_street_atm.next() for cash in range(5)])
['$100', '$100', '$100', '$100', '$100']
>>> hsbc.crisis = True # Crisis is coming, no more money!
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> wall_street_atm = hsbc.create_atm() # It's even true for new ATMs
>>> print(wall_street_atm.next())
<type 'exceptions.StopIteration'>
>>> hsbc.crisis = False # The trouble is, even post-crisis the ATM remains empty
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> brand_new_atm = hsbc.create_atm() # Build a new one to get back in business
>>> for cash in brand_new_atm:
... print cash
$100
$100
$100
$100
$100
$100
$100
$100
$100
...
Nota: Para Python 3, use print(corner_street_atm.__next__())
oprint(next(corner_street_atm))
Puede ser útil para varias cosas como controlar el acceso a un recurso.
Itertools, tu mejor amigo
El módulo itertools contiene funciones especiales para manipular iterables. ¿Alguna vez quisiste duplicar un generador? ¿Cadena de dos generadores? ¿Agrupar valores en una lista anidada con una línea? Map / Zip
sin crear otra lista?
Entonces solo import itertools
.
¿Un ejemplo? Veamos las posibles órdenes de llegada para una carrera de cuatro caballos:
>>> horses = [1, 2, 3, 4]
>>> races = itertools.permutations(horses)
>>> print(races)
<itertools.permutations object at 0xb754f1dc>
>>> print(list(itertools.permutations(horses)))
[(1, 2, 3, 4),
(1, 2, 4, 3),
(1, 3, 2, 4),
(1, 3, 4, 2),
(1, 4, 2, 3),
(1, 4, 3, 2),
(2, 1, 3, 4),
(2, 1, 4, 3),
(2, 3, 1, 4),
(2, 3, 4, 1),
(2, 4, 1, 3),
(2, 4, 3, 1),
(3, 1, 2, 4),
(3, 1, 4, 2),
(3, 2, 1, 4),
(3, 2, 4, 1),
(3, 4, 1, 2),
(3, 4, 2, 1),
(4, 1, 2, 3),
(4, 1, 3, 2),
(4, 2, 1, 3),
(4, 2, 3, 1),
(4, 3, 1, 2),
(4, 3, 2, 1)]
Comprender los mecanismos internos de iteración
La iteración es un proceso que implica iterables (implementando el __iter__()
método) e iteradores (implementando el __next__()
método). Iterables son los objetos de los que puede obtener un iterador. Los iteradores son objetos que le permiten iterar en iterables.
Hay más sobre esto en este artículo sobre cómo for
funcionan los bucles .
yield
esta respuesta no es tan mágica como sugiere. Cuando llama a una función que contiene unayield
declaración en cualquier lugar, obtiene un objeto generador, pero no se ejecuta ningún código. Luego, cada vez que extrae un objeto del generador, Python ejecuta código en la función hasta que se trata de unayield
declaración, luego hace una pausa y entrega el objeto. Cuando extrae otro objeto, Python se reanuda justo despuésyield
y continúa hasta que alcanza otroyield
(a menudo el mismo, pero una iteración más adelante). Esto continúa hasta que la función se ejecuta al final, en cuyo punto el generador se considera agotado.()
lugar de[]
, específicamente, qué()
es (puede haber confusión con una tupla).return
instrucción. (return
está permitido en una función que contieneyield
, siempre que no especifique un valor de retorno.)Atajo para entender
yield
Cuando vea una función con
yield
declaraciones, aplique este sencillo truco para comprender lo que sucederá:result = []
al comienzo de la función.yield expr
conresult.append(expr)
.return result
en la parte inferior de la función.yield
declaraciones! Leer y descifrar el código.Este truco puede darle una idea de la lógica detrás de la función, pero lo que realmente sucede
yield
es significativamente diferente de lo que sucede en el enfoque basado en listas. En muchos casos, el enfoque de rendimiento será mucho más eficiente en memoria y más rápido también. En otros casos, este truco te atrapará en un bucle infinito, aunque la función original funcione bien. Sigue leyendo para aprender más ...No confunda sus Iterables, Iteradores y Generadores.
Primero el protocolo iterador : cuando escribes
Python realiza los siguientes dos pasos:
Obtiene un iterador para
mylist
:Llamar
iter(mylist)
-> esto devuelve un objeto con unnext()
método (o__next__()
en Python 3).[Este es el paso sobre el que la mayoría de la gente se olvida de contarte]
Utiliza el iterador para recorrer los elementos:
Siga llamando al
next()
método en el iterador devuelto desde el paso 1. El valor de retorno denext()
se asigna ax
y se ejecuta el cuerpo del bucle. Si se genera una excepciónStopIteration
desde adentronext()
, significa que no hay más valores en el iterador y que se cierra el ciclo.La verdad es que Python realiza los dos pasos anteriores en cualquier momento que quiera recorrer el contenido de un objeto, por lo que podría ser un ciclo for, pero también podría ser un código como
otherlist.extend(mylist)
(dondeotherlist
hay una lista de Python).Aquí
mylist
hay un iterable porque implementa el protocolo iterador. En una clase definida por el usuario, puede implementar el__iter__()
método para hacer que las instancias de su clase sean iterables. Este método debería devolver un iterador . Un iterador es un objeto con unnext()
método. Es posible implementar ambos__iter__()
ynext()
en la misma clase, y tener__iter__()
retornoself
. Esto funcionará para casos simples, pero no cuando desee que dos iteradores se repitan sobre el mismo objeto al mismo tiempo.Ese es el protocolo iterador, muchos objetos implementan este protocolo:
__iter__()
.Tenga en cuenta que un
for
bucle no sabe con qué tipo de objeto se trata: solo sigue el protocolo iterador y se complace en obtener elemento tras elemento a medida que llamanext()
. Las listas incorporadas devuelven sus elementos uno por uno, los diccionarios devuelven las claves una por una, los archivos devuelven las líneas una por una, etc. Y los generadores regresan ... bueno, ahí es dondeyield
entra:En lugar de
yield
sentencias, si tuviera tresreturn
sentencias,f123()
solo la primera se ejecutaría y la función saldría. Perof123()
no es una función ordinaria. Cuandof123()
se llama, ¡ no devuelve ninguno de los valores en las declaraciones de rendimiento! Devuelve un objeto generador. Además, la función realmente no sale, entra en un estado suspendido. Cuando elfor
bucle intenta recorrer el objeto generador, la función se reanuda desde su estado suspendido en la línea siguiente después deyield
que regresó previamente, ejecuta la siguiente línea de código, en este caso, unayield
declaración, y la devuelve como el siguiente articulo. Esto sucede hasta que sale la función, en cuyo punto se eleva el generadorStopIteration
y sale el bucle.Entonces, el objeto generador es algo así como un adaptador: en un extremo exhibe el protocolo iterador, al exponer
__iter__()
ynext()
métodos para mantener elfor
ciclo feliz. Sin embargo, en el otro extremo, ejecuta la función lo suficiente como para obtener el siguiente valor y la vuelve a poner en modo suspendido.¿Por qué usar generadores?
Por lo general, puede escribir código que no use generadores pero implemente la misma lógica. Una opción es usar la lista temporal 'truco' que mencioné antes. Eso no funcionará en todos los casos, por ejemplo, si tiene bucles infinitos, o puede hacer un uso ineficiente de la memoria cuando tiene una lista realmente larga. El otro enfoque es implementar una nueva clase iterable SomethingIter que mantenga el estado en los miembros de la instancia y realice el siguiente paso lógico en su método
next()
(o__next__()
en Python 3). Dependiendo de la lógica, el código dentro delnext()
método puede terminar pareciendo muy complejo y propenso a errores. Aquí los generadores proporcionan una solución limpia y fácil.fuente
send
ingresar a un generador, que es una gran parte del objetivo de los generadores?otherlist.extend(mylist)
" -> Esto es incorrecto.extend()
modifica la lista en el lugar y no devuelve un iterable. Intentar hacer un bucleotherlist.extend(mylist)
fallará con unTypeError
porqueextend()
regresa implícitamenteNone
y no puede hacerloNone
.mylist
(no enotherlist
) cuando se ejecutaotherlist.extend(mylist)
.Piénsalo de esta manera:
Un iterador es solo un término elegante para un objeto que tiene un
next()
método. Entonces, una función de rendimiento termina siendo algo como esto:Versión original:
Esto es básicamente lo que hace el intérprete de Python con el código anterior:
Para obtener más información sobre lo que sucede detrás de escena, el
for
ciclo se puede reescribir a esto:¿Tiene más sentido o simplemente te confunde más? :)
Debo señalar que se trata de una simplificación excesiva con fines ilustrativos. :)
fuente
__getitem__
podría definirse en lugar de__iter__
. Por ejemplo:,class it: pass; it.__getitem__ = lambda self, i: i*10 if i < 10 else [][0]; for i in it(): print(i)
Imprimirá: 0, 10, 20, ..., 90iterator = some_function()
, la variableiterator
ya no tiene una función llamadanext()
, sino solo una__next__()
función. Pensé en mencionarlo.for
llama la implementación de bucle que escribió el__iter__
método deiterator
, la instancia instanciada deit
?La
yield
palabra clave se reduce a dos hechos simples:yield
palabra clave en cualquier lugar dentro de una función, esa función ya no regresa a través de lareturn
declaración. En cambio , inmediatamente devuelve un objeto "lista pendiente" perezosa llamado generadorlist
orset
orange
dict, con un protocolo incorporado para visitar cada elemento en un orden determinado .En pocas palabras: un generador es una lista perezosa, pendiente de incremento , y las
yield
declaraciones le permiten usar la notación de función para programar los valores de la lista que el generador debe escupir gradualmente.Ejemplo
Definamos una función
makeRange
que sea igual a la de Pythonrange
. LlamarmakeRange(n)
DEVUELVE UN GENERADOR:Para forzar al generador a que devuelva inmediatamente sus valores pendientes, puede pasarlo
list()
(como lo haría con cualquier iterable):Ejemplo comparativo a "solo devolver una lista"
El ejemplo anterior se puede considerar como simplemente crear una lista a la que se agrega y se devuelve:
Sin embargo, hay una gran diferencia; ver la última sección
Cómo podrías usar generadores
Un iterable es la última parte de la comprensión de una lista, y todos los generadores son iterables, por lo que a menudo se usan así:
Para tener una mejor idea de los generadores, puede jugar con el
itertools
módulo (asegúrese de usarlo enchain.from_iterable
lugar dechain
cuando esté justificado). Por ejemplo, incluso podría usar generadores para implementar listas perezosas infinitamente largas comoitertools.count()
. Puede implementar el suyo propiodef enumerate(iterable): zip(count(), iterable)
o, alternativamente, hacerlo con layield
palabra clave en un ciclo while.Tenga en cuenta que los generadores se pueden utilizar para muchas cosas más, como la implementación de corutinas o programación no determinista u otras cosas elegantes. Sin embargo, el punto de vista de "listas perezosas" que presento aquí es el uso más común que encontrará.
Entre bastidores
Así es como funciona el "protocolo de iteración de Python". Es decir, lo que sucede cuando lo haces
list(makeRange(5))
. Esto es lo que describí anteriormente como una "lista lenta e incremental".La función incorporada
next()
solo llama a la.next()
función de objetos , que es parte del "protocolo de iteración" y se encuentra en todos los iteradores. Puede usar manualmente lanext()
función (y otras partes del protocolo de iteración) para implementar cosas sofisticadas, generalmente a expensas de la legibilidad, así que trate de evitar hacer eso ...Minucias
Normalmente, a la mayoría de las personas no les importarían las siguientes distinciones y probablemente quieran dejar de leer aquí.
En Python-speak, un iterable es cualquier objeto que "entiende el concepto de un ciclo for" como una lista
[1,2,3]
, y un iterador es una instancia específica del tipo for-loop solicitado[1,2,3].__iter__()
. Un generador es exactamente igual a cualquier iterador, excepto por la forma en que fue escrito (con la sintaxis de la función).Cuando solicita un iterador de una lista, crea un nuevo iterador. Sin embargo, cuando solicita un iterador de un iterador (lo que rara vez haría), solo le da una copia de sí mismo.
Por lo tanto, en el improbable caso de que no esté haciendo algo como esto ...
... luego recuerda que un generador es un iterador ; es decir, es de un solo uso. Si desea reutilizarlo, debe llamar
myRange(...)
nuevamente. Si necesita usar el resultado dos veces, conviértalo en una lista y guárdelo en una variablex = list(myRange(5))
. Aquellos que absolutamente necesitan clonar un generador (por ejemplo, que están haciendo una metaprogramación terriblemente hack) pueden usaritertools.tee
si es absolutamente necesario, ya que la propuesta de estándares de Python PEP de iterador copiable ha sido diferida.fuente
Esquema de respuesta / resumen
yield
, cuando se llama, devuelve un generador .yield from
.return
en un generador).Generadores:
yield
solo es legal dentro de una definición de función, y la inclusiónyield
en una definición de función hace que devuelva un generador.La idea de los generadores proviene de otros idiomas (ver nota 1) con implementaciones variables. En Python's Generators, la ejecución del código está congelada. en el punto del rendimiento. Cuando se llama al generador (los métodos se analizan a continuación), la ejecución se reanuda y luego se congela en el siguiente rendimiento.
yield
proporciona una manera fácil de implementar el protocolo iterador , definido por los dos métodos siguientes:__iter__
ynext
(Python 2) o__next__
(Python 3). Ambos métodos hacen que un objeto sea un iterador que podría verificar con laIterator
Clase base abstracta delcollections
módulo.El tipo de generador es un subtipo de iterador:
Y si es necesario, podemos verificar de esta manera:
Una característica de un
Iterator
es que una vez agotado , no puede reutilizarlo o restablecerlo:Tendrá que hacer otro si desea volver a utilizar su funcionalidad (consulte la nota 2):
Uno puede generar datos mediante programación, por ejemplo:
El generador simple anterior también es equivalente al siguiente: a partir de Python 3.3 (y no está disponible en Python 2), puede usar
yield from
:Sin embargo,
yield from
también permite la delegación a subgeneradores, que se explicará en la siguiente sección sobre delegación cooperativa con subcorrinas.Corutinas
yield
forma una expresión que permite enviar datos al generador (ver nota 3)Aquí hay un ejemplo, tome nota de la
received
variable, que apuntará a los datos que se envían al generador:En primer lugar, hay que poner en cola el generador con la función incorporada,
next
. Llamará al métodonext
o__next__
método apropiado , dependiendo de la versión de Python que esté utilizando:Y ahora podemos enviar datos al generador. ( Enviar
None
es lo mismo que llamarnext
):Delegación Cooperativa a Sub-Corutina con
yield from
Ahora, recuerde que
yield from
está disponible en Python 3. Esto nos permite delegar corutinas a una subcorutina:Y ahora podemos delegar la funcionalidad a un subgenerador y puede ser utilizado por un generador como el anterior:
Puede leer más sobre la semántica precisa de
yield from
en PEP 380.Otros métodos: cerrar y tirar
El
close
método aumentaGeneratorExit
en el punto en que se ejecutó la ejecución de la función. Esto también se llamará__del__
para que pueda poner cualquier código de limpieza donde maneje elGeneratorExit
:También puede lanzar una excepción que puede manejarse en el generador o propagarse al usuario:
Conclusión
Creo que he cubierto todos los aspectos de la siguiente pregunta:
Resulta que
yield
hace mucho. Estoy seguro de que podría agregar ejemplos aún más completos a esto. Si desea más o tiene alguna crítica constructiva, hágamelo saber comentando a continuación.Apéndice:
Crítica de la respuesta principal / aceptada **
__iter__
método que devuelve un iterador . Un iterador proporciona un método.next
(Python 2 o.__next__
(Python 3), que losfor
bucles llaman implícitamente hasta que se elevaStopIteration
, y una vez que lo hace, continuará haciéndolo.yield
parte..next
método, cuando en lugar de eso debería utilizar la función interna,next
. Sería una capa apropiada de indirección, porque su código no funciona en Python 3.yield
hace en absoluto.yield
proporciona junto con la nueva funcionalidadyield from
en Python 3. La respuesta superior / aceptada es una respuesta muy incompleta.Crítica de la respuesta que sugiere
yield
en una expresión o comprensión generadora.La gramática actualmente permite cualquier expresión en una lista de comprensión.
Dado que el rendimiento es una expresión, algunos lo han promocionado como interesante para usarlo en comprensiones o expresiones generadoras, a pesar de no mencionar ningún caso de uso particularmente bueno.
Los desarrolladores principales de CPython están discutiendo la depreciación de su asignación . Aquí hay una publicación relevante de la lista de correo:
Además, hay un problema pendiente (10544) que parece apuntar en la dirección de que esto nunca es una buena idea (PyPy, una implementación de Python escrita en Python, ya está generando advertencias de sintaxis).
En pocas palabras , hasta que los desarrolladores de CPython nos digan lo contrario: no pongas
yield
una expresión o comprensión generadora.La
return
declaración en un generadorEn Python 2 :
Una
expression_list
es, básicamente, cualquier número de expresiones separadas por comas - esencialmente, en Python 2, puede detener el generador conreturn
, pero no se puede devolver un valor.En Python 3 :
Notas al pie
Se hizo referencia a los lenguajes CLU, Sather e Icon en la propuesta para introducir el concepto de generadores a Python. La idea general es que una función puede mantener el estado interno y generar puntos de datos intermedios a pedido del usuario. Esto prometió ser superior en rendimiento a otros enfoques, incluido el subprocesamiento de Python , que ni siquiera está disponible en algunos sistemas.
Esto significa, por ejemplo, que los
xrange
objetos (range
en Python 3) no sonIterator
s, aunque sean iterables, porque pueden reutilizarse. Al igual que las listas, sus__iter__
métodos devuelven objetos iteradores.yield
se introdujo originalmente como una declaración, lo que significa que solo podía aparecer al comienzo de una línea en un bloque de código. Ahorayield
crea una expresión de rendimiento. https://docs.python.org/2/reference/simple_stmts.html#grammar-token-yield_stmt Este cambio se propuso para permitir que un usuario envíe datos al generador tal como uno podría recibirlos. Para enviar datos, uno debe poder asignarlos a algo, y para eso, una declaración simplemente no funcionará.fuente
yield
es igualreturn
que: devuelve todo lo que le dices (como generador). La diferencia es que la próxima vez que llame al generador, la ejecución comienza desde la última llamada a layield
declaración. A diferencia del regreso, el marco de la pila no se limpia cuando se produce un rendimiento, sin embargo, el control se transfiere nuevamente al llamante, por lo que su estado se reanudará la próxima vez que se llame a la función.En el caso de su código, la función
get_child_candidates
actúa como un iterador, de modo que cuando extiende su lista, agrega un elemento a la vez a la nueva lista.list.extend
llama a un iterador hasta que se agota. En el caso de la muestra de código que publicó, sería mucho más claro devolver una tupla y agregarla a la lista.fuente
Hay una cosa más que mencionar: una función que rinde en realidad no tiene que terminar. He escrito un código como este:
Entonces puedo usarlo en otro código como este:
Realmente ayuda a simplificar algunos problemas y facilita el trabajo con algunas cosas.
fuente
Para aquellos que prefieren un ejemplo de trabajo mínimo, mediten en esta sesión interactiva de Python:
fuente
TL; DR
En lugar de esto:
hacer esto:
Siempre que te encuentres construyendo una lista desde cero,
yield
cada pieza en su lugar.Este fue mi primer momento "ajá" con rendimiento.
yield
es una manera azucarada de decirMismo comportamiento:
Comportamiento diferente:
El rendimiento es de un solo paso : solo puede iterar una vez. Cuando una función tiene un rendimiento, la llamamos función generadora . Y un iterador es lo que devuelve. Esos términos son reveladores. Perdemos la conveniencia de un contenedor, pero obtenemos el poder de una serie que se calcula según sea necesario y arbitrariamente larga.
El rendimiento es perezoso , pospone el cálculo. Una función con rendimiento no se ejecuta en absoluto cuando la llamas. Devuelve un objeto iterador que recuerda dónde lo dejó. Cada vez que llama
next()
al iterador (esto sucede en un bucle for) la ejecución avanza pulgadas hasta el siguiente rendimiento.return
plantea StopIteration y finaliza la serie (este es el final natural de un ciclo for).El rendimiento es versátil . Los datos no tienen que almacenarse todos juntos, pueden estar disponibles uno a la vez. Puede ser infinito.
Si necesitas varios pases y la serie no es demasiado larga, simplemente instálela
list()
:Elección brillante de la palabra
yield
porque se aplican ambos significados :... proporcione los siguientes datos de la serie.
... renunciar a la ejecución de la CPU hasta que el iterador avance.
fuente
El rendimiento te da un generador.
Como puedes ver, en el primer caso
foo
guarda la lista completa en la memoria a la vez. No es un gran problema para una lista con 5 elementos, pero ¿qué pasa si quieres una lista de 5 millones? No solo es un gran devorador de memoria, sino que también cuesta mucho tiempo construirlo en el momento en que se llama a la función.En el segundo caso,
bar
solo te da un generador. Un generador es iterable, lo que significa que puede usarlo en unfor
ciclo, etc., pero a cada valor solo se puede acceder una vez. Todos los valores tampoco se almacenan en la memoria al mismo tiempo; el objeto generador "recuerda" dónde estaba en el bucle la última vez que lo llamó, de esta manera, si está usando un iterable para (digamos) contar hasta 50 mil millones, no tiene que contar hasta 50 mil millones a la vez y almacenar los 50 mil millones de números para contar.Nuevamente, este es un ejemplo bastante ingenioso, probablemente usaría itertools si realmente quisiera contar hasta 50 mil millones. :)
Este es el caso de uso más simple de generadores. Como dijiste, se puede usar para escribir permutaciones eficientes, usando el rendimiento para impulsar las cosas a través de la pila de llamadas en lugar de usar algún tipo de variable de pila. Los generadores también se pueden utilizar para atravesar árboles especializados y todo tipo de otras cosas.
fuente
range
también devuelve un generador en lugar de una lista, por lo que también vería una idea similar, excepto que__repr__
/__str__
se anula para mostrar un mejor resultado, en este casorange(1, 10, 2)
.Está devolviendo un generador. No estoy particularmente familiarizado con Python, pero creo que es el mismo tipo de cosas que los bloques iteradores de C # si estás familiarizado con ellos.
La idea clave es que el compilador / intérprete / lo que sea haga algún truco para que, en lo que respecta a la persona que llama, pueda seguir llamando a next () y seguirá devolviendo valores, como si el método generador estuviera en pausa . Ahora, obviamente, no puede realmente "pausar" un método, por lo que el compilador construye una máquina de estados para que recuerde dónde se encuentra actualmente y cómo son las variables locales, etc. Esto es mucho más fácil que escribir un iterador usted mismo.
fuente
Hay un tipo de respuesta que no creo que se haya dado todavía, entre las muchas respuestas geniales que describen cómo usar generadores. Aquí está la respuesta de la teoría del lenguaje de programación:
La
yield
declaración en Python devuelve un generador. Un generador en Python es una función que devuelve continuaciones (y específicamente un tipo de corutina, pero las continuaciones representan el mecanismo más general para comprender lo que está sucediendo).Las continuaciones en la teoría de lenguajes de programación son un tipo de cálculo mucho más fundamental, pero no se usan con frecuencia, porque son extremadamente difíciles de razonar y también muy difíciles de implementar. Pero la idea de lo que es una continuación es directa: es el estado de un cálculo que aún no ha terminado. En este estado, se guardan los valores actuales de las variables, las operaciones que aún no se han realizado, etc. Luego, en algún momento posterior en el programa, se puede invocar la continuación, de modo que las variables del programa se restablezcan a ese estado y se lleven a cabo las operaciones que se guardaron.
Las continuaciones, en esta forma más general, se pueden implementar de dos maneras. En el
call/cc
camino, la pila del programa se guarda literalmente y luego, cuando se invoca la continuación, se restaura la pila.En el estilo de paso de continuación (CPS), las continuaciones son solo funciones normales (solo en lenguajes donde las funciones son de primera clase) que el programador administra y pasa explícitamente a las subrutinas. En este estilo, el estado del programa se representa mediante cierres (y las variables que están codificadas en ellos) en lugar de variables que residen en algún lugar de la pila. Las funciones que administran el flujo de control aceptan la continuación como argumentos (en algunas variaciones de CPS, las funciones pueden aceptar múltiples continuaciones) y manipulan el flujo de control invocandolos simplemente llamándolos y regresando después. Un ejemplo muy simple de estilo de paso de continuación es el siguiente:
En este ejemplo (muy simplista), el programador guarda la operación de escribir realmente el archivo en una continuación (que potencialmente puede ser una operación muy compleja con muchos detalles para escribir), y luego pasa esa continuación (es decir, como primer paso). cierre de clase) a otro operador que procesa un poco más y luego lo llama si es necesario. (Uso mucho este patrón de diseño en la programación de la GUI real, ya sea porque me ahorra líneas de código o, lo que es más importante, para administrar el flujo de control después de que se desencadenan los eventos de la GUI).
El resto de esta publicación, sin pérdida de generalidad, conceptualizará las continuaciones como CPS, porque es muchísimo más fácil de entender y leer.
Ahora hablemos de generadores en Python. Los generadores son un subtipo específico de continuación. Mientras que las continuaciones pueden en general guardar el estado de un cálculo (es decir, la pila de llamadas del programa), los generadores solo pueden guardar el estado de la iteración sobre un iterador . Aunque, esta definición es ligeramente engañosa para ciertos casos de uso de generadores. Por ejemplo:
Este es claramente un iterable razonable cuyo comportamiento está bien definido: cada vez que el generador itera sobre él, devuelve 4 (y lo hace para siempre). Pero no es probablemente el tipo prototípico de iterable que viene a la mente cuando se piensa en iteradores (es decir,
for x in collection: do_something(x)
). Este ejemplo ilustra el poder de los generadores: si algo es un iterador, un generador puede guardar el estado de su iteración.Para reiterar: las continuaciones pueden guardar el estado de la pila de un programa y los generadores pueden guardar el estado de la iteración. Esto significa que las continuaciones son mucho más poderosas que los generadores, pero también que los generadores son mucho más fáciles. Son más fáciles de implementar para el diseñador de lenguaje, y son más fáciles de usar para el programador (si tiene algo de tiempo para grabar, intente leer y comprender esta página sobre las continuaciones y la llamada / cc ).
Pero podría implementar fácilmente (y conceptualizar) generadores como un caso simple y específico de estilo de paso de continuación:
Cada vez que
yield
se llama, le dice a la función que devuelva una continuación. Cuando se vuelve a llamar a la función, comienza desde donde la dejó. Entonces, en pseudo-pseudocódigo (es decir, no pseudocódigo, pero no código), elnext
método del generador es básicamente el siguiente:donde la
yield
palabra clave es en realidad azúcar sintáctica para la función de generador real, básicamente algo como:Recuerde que esto es solo pseudocódigo y la implementación real de generadores en Python es más compleja. Pero como un ejercicio para comprender lo que está sucediendo, intente usar el estilo de paso continuo para implementar objetos generadores sin usar la
yield
palabra clave.fuente
Aquí hay un ejemplo en lenguaje sencillo. Proporcionaré una correspondencia entre los conceptos humanos de alto nivel y los conceptos de Python de bajo nivel.
Quiero operar en una secuencia de números, pero no quiero molestarme con la creación de esa secuencia, solo quiero centrarme en la operación que quiero hacer. Entonces, hago lo siguiente:
Este paso corresponde a
def
la función del generador, es decir, la función que contiene ayield
.Este paso corresponde a llamar a la función de generador que devuelve un objeto generador. Tenga en cuenta que todavía no me dice ningún número; solo agarra tu papel y lápiz.
Este paso corresponde a llamar
.next()
al objeto generador.Este paso corresponde al objeto generador que finaliza su trabajo y genera una
StopIteration
excepción La función de generador no necesita generar la excepción. Se genera automáticamente cuando la función finaliza o emite areturn
.Esto es lo que hace un generador (una función que contiene a
yield
); comienza a ejecutarse, se detiene cada vez que hace unyield
, y cuando se le solicita un.next()
valor, continúa desde el punto en que fue el último. Encaja perfectamente por diseño con el protocolo iterador de Python, que describe cómo solicitar valores secuencialmente.El usuario más famoso del protocolo iterador es el
for
comando en Python. Entonces, cada vez que haces un:no importa si
sequence
es una lista, una cadena, un diccionario o un objeto generador como se describe anteriormente; el resultado es el mismo: lee los elementos de una secuencia uno por uno.Tenga en cuenta que
def
introducir una función que contiene unayield
palabra clave no es la única forma de crear un generador; Es la forma más fácil de crear uno.Para obtener información más precisa, lea sobre los tipos de iteradores , la declaración de rendimiento y los generadores en la documentación de Python.
fuente
Si bien muchas respuestas muestran por qué usarías un
yield
para crear un generador, hay más usos parayield
. Es bastante fácil hacer una rutina, que permite el paso de información entre dos bloques de código. No repetiré ninguno de los buenos ejemplos que ya se han dado sobre el usoyield
para crear un generador.Para ayudar a comprender qué
yield
hace a en el siguiente código, puede usar su dedo para rastrear el ciclo a través de cualquier código que tenga unyield
. Cada vez que toca con el dedoyield
, debe esperar a que se ingrese anext
osend
a. Cuandonext
se llama a, se rastrea el código hasta que se golpea elyield
... el código a la derecha delyield
se evalúa y se devuelve a la persona que llama ... luego espera. Cuandonext
se vuelve a llamar, realiza otro ciclo a través del código. Sin embargo, notará que en una rutina,yield
también se puede usar con unsend
... que enviará un valor de la persona que llama a la función de rendimiento. Si unsend
se da un, entoncesyield
recibe el valor enviado y lo escupe por el lado izquierdo ... luego la traza a través del código progresa hasta queyield
vuelva a presionar el botón (devolviendo el valor al final, como sinext
se hubiera llamado).Por ejemplo:
fuente
Hay otro
yield
uso y significado (desde Python 3.3):De PEP 380 - Sintaxis para delegar a un subgenerador :
Además, esto introducirá (desde Python 3.5):
para evitar que las corutinas se confundan con un generador regular (hoy
yield
se usa en ambos).fuente
Todas excelentes respuestas, aunque un poco difíciles para los novatos.
Supongo que has aprendido la
return
declaración.Como analogía,
return
yyield
son gemelos.return
significa 'regresar y parar' mientras que 'ceder' significa 'regresar, pero continuar'Ejecutarlo:
Mira, solo obtienes un solo número en lugar de una lista de ellos.
return
nunca le permite prevalecer felizmente, solo implemente una vez y salga.Reemplazar
return
conyield
:Ahora ganas para obtener todos los números.
En comparación con el
return
que se ejecuta una vez y se detiene, seyield
ejecutan los tiempos planeados. Puedes interpretarreturn
comoreturn one of them
yyield
comoreturn all of them
. Esto se llamaiterable
.Se trata del núcleo
yield
.La diferencia entre las
return
salidas de una lista y layield
salida del objeto es:Siempre obtendrá [0, 1, 2] de un objeto de lista, pero solo podrá recuperarlos de 'la
yield
salida del objeto ' una vez. Por lo tanto, tiene un nuevogenerator
objeto de nombre como se muestra enOut[11]: <generator object num_list at 0x10327c990>
.En conclusión, como metáfora para asimilarlo:
return
yyield
son gemeloslist
ygenerator
son gemelosfuente
yield
. Esto es importante, creo, y debería expresarse.Aquí hay algunos ejemplos de Python sobre cómo implementar realmente generadores como si Python no les proporcionara azúcar sintáctico:
Como generador de Python:
Usando cierres léxicos en lugar de generadores
Uso de cierres de objetos en lugar de generadores (porque ClosuresAndObjectsAreEquivalent )
fuente
Iba a publicar "lea la página 19 de 'Python: Referencia esencial' de Beazley para obtener una descripción rápida de los generadores", pero muchos otros ya han publicado buenas descripciones.
Además, tenga en cuenta que
yield
se puede usar en las rutinas como el doble de su uso en las funciones del generador. Aunque no es el mismo uso que el fragmento de código,(yield)
se puede usar como una expresión en una función. Cuando una persona que llama envía un valor al método utilizando elsend()
método, la rutina se ejecutará hasta(yield)
que se encuentre la siguiente instrucción.Los generadores y las rutinas son una forma genial de configurar aplicaciones de tipo flujo de datos. Pensé que valdría la pena saber sobre el otro uso de la
yield
declaración en las funciones.fuente
Desde el punto de vista de la programación, los iteradores se implementan como thunks .
Para implementar iteradores, generadores y grupos de subprocesos para ejecución concurrente, etc. como thunks (también llamados funciones anónimas), uno usa mensajes enviados a un objeto de cierre, que tiene un despachador, y el despachador responde a "mensajes".
http://en.wikipedia.org/wiki/Message_passing
" siguiente " es un mensaje enviado a un cierre, creado por la llamada " iter ".
Hay muchas formas de implementar este cálculo. Utilicé la mutación, pero es fácil hacerlo sin mutación, devolviendo el valor actual y el siguiente rendimiento.
Aquí hay una demostración que utiliza la estructura de R6RS, pero la semántica es absolutamente idéntica a la de Python. Es el mismo modelo de cálculo, y solo se requiere un cambio en la sintaxis para reescribirlo en Python.
fuente
Aquí hay un ejemplo simple:
Salida:
No soy un desarrollador de Python, pero me parece que
yield
mantiene la posición de flujo del programa y el siguiente ciclo comienza desde la posición "rendimiento". Parece que está esperando en esa posición, y justo antes de eso, devuelve un valor afuera, y la próxima vez continúa funcionando.Parece ser una habilidad interesante y agradable: D
fuente
Aquí hay una imagen mental de lo que
yield
hace.Me gusta pensar que un hilo tiene una pila (incluso cuando no está implementado de esa manera).
Cuando se llama a una función normal, coloca sus variables locales en la pila, realiza algunos cálculos, luego borra la pila y regresa. Los valores de sus variables locales nunca se vuelven a ver.
Con una
yield
función, cuando su código comienza a ejecutarse (es decir, después de que se llama a la función, devolviendo un objeto generador, cuyonext()
método se invoca luego), de manera similar coloca sus variables locales en la pila y calcula durante un tiempo. Pero luego, cuando llega a layield
declaración, antes de borrar su parte de la pila y regresar, toma una instantánea de sus variables locales y las almacena en el objeto generador. También escribe el lugar donde está actualmente en su código (es decir, layield
declaración particular ).Por lo tanto, es una especie de función congelada en la que se cuelga el generador.
Cuando
next()
se llama posteriormente, recupera las pertenencias de la función en la pila y la vuelve a animar. La función continúa calculando desde donde se detuvo, ajena al hecho de que acababa de pasar una eternidad en almacenamiento en frío.Compare los siguientes ejemplos:
Cuando llamamos a la segunda función, se comporta de manera muy diferente a la primera. La
yield
declaración puede ser inalcanzable, pero si está presente en algún lugar, cambia la naturaleza de lo que estamos tratando.Las llamadas
yielderFunction()
no ejecutan su código, pero hacen un generador del código. (Tal vez sea una buena idea nombrar tales cosas con elyielder
prefijo de legibilidad).Los campos
gi_code
ygi_frame
son donde se almacena el estado congelado. Al explorarlosdir(..)
, podemos confirmar que nuestro modelo mental anterior es creíble.fuente
Como cada respuesta sugiere,
yield
se usa para crear un generador de secuencia. Se utiliza para generar alguna secuencia dinámicamente. Por ejemplo, mientras lee un archivo línea por línea en una red, puede usar layield
función de la siguiente manera:Puede usarlo en su código de la siguiente manera:
Control de Ejecución Transferencia gotcha
El control de ejecución se transferirá de getNextLines () al
for
bucle cuando se ejecute el rendimiento. Por lo tanto, cada vez que se invoca getNextLines (), la ejecución comienza desde el punto donde se detuvo la última vez.En resumen, una función con el siguiente código
imprimirá
fuente
Un ejemplo sencillo para entender de qué se trata:
yield
El resultado es:
fuente
print(i, end=' ')
? De lo contrario, creo que el comportamiento predeterminado colocaría cada número en una nueva línea(Mi respuesta a continuación solo habla desde la perspectiva del uso del generador Python, no de la implementación subyacente del mecanismo generador , que implica algunos trucos de manipulación de pila y montón).
Cuando
yield
se usa en lugar de unareturn
función de python, esa función se convierte en algo especial llamadogenerator function
. Esa función devolverá un objeto degenerator
tipo. Layield
palabra clave es una bandera para notificar al compilador de Python para que trate dicha función especialmente. Las funciones normales finalizarán una vez que se devuelva algún valor. Pero con la ayuda del compilador, la función del generador puede considerarse como reanudable. Es decir, el contexto de ejecución se restaurará y la ejecución continuará desde la última ejecución. Hasta que llame explícitamente a return, que generará unaStopIteration
excepción (que también es parte del protocolo iterador), o llegará al final de la función. Encontré muchas referencias sobregenerator
esto, pero unodesde elfunctional programming perspective
es el más digerible.(Ahora quiero hablar sobre la lógica subyacente
generator
, yiterator
según mi propia comprensión. Espero que esto pueda ayudarlo a comprender la motivación esencial del iterador y el generador. Tal concepto aparece en otros lenguajes, como C #).Según tengo entendido, cuando queremos procesar un montón de datos, generalmente primero almacenamos los datos en algún lugar y luego los procesamos uno por uno. Pero este enfoque ingenuo es problemático. Si el volumen de datos es enorme, es caro almacenarlos como un todo de antemano. Entonces, en lugar de almacenar el
data
propio directamente, ¿por qué no almacenar algún tipo demetadata
indirectamente, es decirthe logic how the data is computed
.Hay 2 enfoques para envolver dichos metadatos.
as a class
. Este es el llamadoiterator
quién implementa el protocolo iterador (es decir__next__()
, los__iter__()
métodos y ). Este es también el patrón de diseño de iterador comúnmente visto .as a function
. Este es el llamadogenerator function
. Pero bajo el capó, el iteradorgenerator object
aún devueltoIS-A
porque también implementa el protocolo de iterador.De cualquier manera, se crea un iterador, es decir, algún objeto que puede proporcionarle los datos que desea. El enfoque OO puede ser un poco complejo. De todos modos, cuál usar depende de usted.
fuente
En resumen, la
yield
declaración transforma su función en una fábrica que produce un objeto especial llamado agenerator
que envuelve el cuerpo de su función original. Cuandogenerator
se itera, ejecuta su función hasta que alcanza la siguiente,yield
luego suspende la ejecución y evalúa el valor pasadoyield
. Repite este proceso en cada iteración hasta que la ruta de ejecución salga de la función. Por ejemplo,simplemente salidas
La potencia proviene del uso del generador con un bucle que calcula una secuencia, el generador ejecuta el bucle deteniéndose cada vez para 'producir' el siguiente resultado del cálculo, de esta manera calcula una lista sobre la marcha, el beneficio es la memoria guardado para cálculos especialmente grandes
Supongamos que desea crear una
range
función propia que produzca un rango iterable de números, podría hacerlo así,y úsalo así;
Pero esto es ineficiente porque
Afortunadamente, Guido y su equipo fueron lo suficientemente generosos como para desarrollar generadores para que pudiéramos hacer esto;
Ahora, en cada iteración, una función en el generador llamado
next()
ejecuta la función hasta que alcanza una declaración de "rendimiento" en la que se detiene y "produce" el valor o llega al final de la función. En este caso, en la primera llamada, senext()
ejecuta hasta la declaración de rendimiento y el rendimiento 'n', en la siguiente llamada ejecutará la declaración de incremento, volverá al 'while', la evaluará y, si es verdadero, se detendrá y ceder 'n' nuevamente, continuará de esa manera hasta que la condición while vuelva falsa y el generador salte al final de la función.fuente
El rendimiento es un objeto
A
return
en una función devolverá un solo valor.Si desea que una función devuelva un gran conjunto de valores , use
yield
.Más importante aún,
yield
es una barrera .Es decir, ejecutará el código en su función desde el principio hasta que llegue
yield
. Luego, devolverá el primer valor del bucle.Luego, cualquier otra llamada ejecutará el ciclo que ha escrito en la función una vez más, devolviendo el siguiente valor hasta que no haya ningún valor para devolver.
fuente
Muchas personas usan en
return
lugar de hacerloyield
, pero en algunos casosyield
pueden ser más eficientes y más fáciles de trabajar.Aquí hay un ejemplo que
yield
definitivamente es mejor para:Ambas funciones hacen lo mismo, pero
yield
usa tres líneas en lugar de cinco y tiene una variable menos de la que preocuparse.Como puede ver, ambas funciones hacen lo mismo. La única diferencia es que
return_dates()
da una lista yyield_dates()
da un generador.Un ejemplo de la vida real sería algo así como leer un archivo línea por línea o si solo quieres hacer un generador.
fuente
yield
es como un elemento de retorno para una función. La diferencia es que elyield
elemento convierte una función en un generador. Un generador se comporta como una función hasta que algo se "produce". El generador se detiene hasta la próxima llamada y continúa exactamente desde el mismo punto en que comenzó. Puede obtener una secuencia de todos los valores 'cedidos' en uno, llamandolist(generator())
.fuente
La
yield
palabra clave simplemente recopila resultados que regresan. Piensa enyield
comoreturn +=
fuente
Aquí hay un
yield
enfoque simple , para calcular la serie de Fibonacci, explicado:Cuando ingrese esto en su REPL y luego intente llamarlo, obtendrá un resultado desconcertante:
Esto se debe a la presencia de
yield
Python señalado que desea crear un generador , es decir, un objeto que genera valores a pedido.Entonces, ¿cómo se generan estos valores? Esto puede hacerse directamente usando la función incorporada
next
o, indirectamente, al alimentarlo a una construcción que consume valores.Usando la
next()
función incorporada, invoca directamente.next
/__next__
, obligando al generador a producir un valor:Indirectamente, si proporciona
fib
unfor
bucle, unlist
inicializador, untuple
inicializador o cualquier otra cosa que espere un objeto que genere / produzca valores, "consumirá" el generador hasta que no pueda producir más valores (y regrese) :Del mismo modo, con un
tuple
inicializador:Un generador difiere de una función en el sentido de que es vago. Lo logra manteniendo su estado local y permitiéndole reanudar cuando lo necesite.
Cuando invocas
fib
por primera vez llamándolo:Python compila la función, encuentra la
yield
palabra clave y simplemente le devuelve un objeto generador. No muy útil parece.Cuando solicita que genere el primer valor, directa o indirectamente, ejecuta todas las declaraciones que encuentra, hasta que encuentra un
yield
, luego devuelve el valor que proporcionóyield
y hace una pausa. Para un ejemplo que demuestre esto mejor, usemos algunasprint
llamadas (reemplace conprint "text"
if en Python 2):Ahora, ingrese en REPL:
ahora tiene un objeto generador esperando un comando para que genere un valor. Use
next
y vea lo que se imprime:Los resultados sin comillas son lo que se imprime. El resultado citado es el que se devuelve
yield
. Llama denext
nuevo ahora:El generador recuerda que se detuvo
yield value
y se reanuda desde allí. Se imprime el siguiente mensaje y la búsqueda de layield
instrucción para pausar se realiza nuevamente (debido alwhile
bucle).fuente