lenguaje - python tutorial
el propósito de la función "enviar" del generador de pitones? (6)
¿Puede alguien darme un ejemplo de por qué existe la función "enviar" asociada a la función del generador de Python? Entiendo completamente la función de rendimiento. Sin embargo, la función enviar es confusa para mí. La documentación sobre este método es intrincada:
generator.send(value)
Reanuda la ejecución y "envía" un valor a la función del generador. El argumento de valor se convierte en el resultado de la expresión de rendimiento actual. El método send () devuelve el siguiente valor obtenido por el generador, o aumenta StopIteration si el generador sale sin producir otro valor.
Qué significa eso? Pensé que el valor era la entrada a la función? La frase "El método send () devuelve el siguiente valor obtenido por el generador" parece ser también el propósito exacto de la función de rendimiento; rendimiento devuelve el siguiente valor producido por el generador ...
¿Puede alguien darme un ejemplo de un generador que utiliza envío que logra algo que el rendimiento no puede?
Algunos casos de uso para usar generador y send()
Los generadores con send()
permiten:
- recordando el estado interno de la ejecución
- en qué paso estamos
- cuál es el estado actual de nuestros datos
- devolver la secuencia de valores
- recibiendo secuencia de entradas
Aquí hay algunos casos de uso:
Intentó seguir una receta
Déjenos tener una receta, que espera un conjunto predefinido de entradas en algún orden.
Podemos:
- crea una instancia de
watched_attempt
de la receta - déjalo obtener algunas entradas
- con cada información de retorno de entrada sobre lo que está actualmente en el bote
con cada comprobación de entrada, que la entrada es la esperada (y fallar si no es)
def recipe(): pot = [] action = yield pot assert action == ("add", "water") pot.append(action[1]) action = yield pot assert action == ("add", "salt") pot.append(action[1]) action = yield pot assert action == ("boil", "water") action = yield pot assert action == ("add", "pasta") pot.append(action[1]) action = yield pot assert action == ("decant", "water") pot.remove("water") action = yield pot assert action == ("serve") pot = [] yield pot
Para usarlo, primero crea la instancia de watched_attempt
:
>>> watched_attempt = recipe()
>>> watched_attempt.next()
[]
La llamada a .next()
es necesaria para iniciar la ejecución del generador.
El valor devuelto muestra, nuestro pozo está actualmente vacío.
Ahora haga algunas acciones siguiendo lo que la receta espera:
>>> watched_attempt.send(("add", "water"))
[''water'']
>>> watched_attempt.send(("add", "salt"))
[''water'', ''salt'']
>>> watched_attempt.send(("boil", "water"))
[''water'', ''salt'']
>>> watched_attempt.send(("add", "pasta"))
[''water'', ''salt'', ''pasta'']
>>> watched_attempt.send(("decant", "water"))
[''salt'', ''pasta'']
>>> watched_attempt.send(("serve"))
[]
Como vemos, la olla finalmente está vacía.
En el caso, uno no seguiría la receta, fallaría (lo que podría ser el resultado deseado del intento de cocinar algo, simplemente aprendiendo que no prestamos suficiente atención cuando recibimos instrucciones).
>>> watched_attempt = running.recipe()
>>> watched_attempt.next()
[]
>>> watched_attempt.send(("add", "water"))
[''water'']
>>> watched_attempt.send(("add", "pasta"))
---------------------------------------------------------------------------
AssertionError Traceback (most recent call last)
<ipython-input-21-facdf014fe8e> in <module>()
----> 1 watched_attempt.send(("add", "pasta"))
/home/javl/sandbox/stack/send/running.py in recipe()
29
30 action = yield pot
---> 31 assert action == ("add", "salt")
32 pot.append(action[1])
33
AssertionError:
Darse cuenta de:
- hay una secuencia lineal de los pasos esperados
- los pasos pueden diferir (algunos están eliminando, otros están agregando al bote)
- logramos hacer todo eso mediante un generador de funciones: no es necesario utilizar clases complejas o strutures similares.
Correr totales
Podemos usar el generador para realizar un seguimiento del total de valores que se le envían.
Cada vez que agregamos un número, se devuelve el recuento de las entradas y la suma total (válido por el momento en que se envió la entrada anterior).
from collections import namedtuple
RunningTotal = namedtuple("RunningTotal", ["n", "total"])
def runningtotals(n=0, total=0):
while True:
delta = yield RunningTotal(n, total)
if delta:
n += 1
total += delta
if __name__ == "__main__":
nums = [9, 8, None, 3, 4, 2, 1]
bookeeper = runningtotals()
print bookeeper.next()
for num in nums:
print num, bookeeper.send(num)
La salida se vería así:
RunningTotal(n=0, total=0)
9 RunningTotal(n=1, total=9)
8 RunningTotal(n=2, total=17)
None RunningTotal(n=2, total=17)
3 RunningTotal(n=3, total=20)
4 RunningTotal(n=4, total=24)
2 RunningTotal(n=5, total=26)
1 RunningTotal(n=6, total=27)
El método de send
implementa coroutines .
Si no se ha encontrado con Corutinas, son difíciles de entender porque cambian la forma en que fluye el programa. Puede leer un buen tutorial para más detalles.
Esta función es escribir corutinas
def coroutine():
for i in range(1, 10):
print("From generator {}".format((yield i)))
c = coroutine()
c.send(None)
try:
while True:
print("From user {}".format(c.send(1)))
except StopIteration: pass
huellas dactilares
From generator 1
From user 2
From generator 1
From user 3
From generator 1
From user 4
...
¿Ves cómo se pasa el control de un lado a otro? Esas son corrutinas. Se pueden usar para todo tipo de cosas interesantes como asynch IO y similares.
Piénsalo así, con un generador y sin enviar, es una calle de un solo sentido
========== yield ========
Generator | ------------> | User |
========== ========
Pero con enviar, se convierte en una calle de dos vías
========== yield ========
Generator | ------------> | User |
========== <------------ ========
send
Lo que abre la puerta para que el usuario personalice el comportamiento de los generadores sobre la marcha y el generador responda al usuario.
Esto puede ayudar a alguien. Aquí hay un generador que no se ve afectado por la función de envío. Toma el parámetro de número en la instanciación y no se ve afectado por el envío:
>>> def double_number(number):
... while True:
... number *=2
... yield number
...
>>> c = double_number(4)
>>> c.send(None)
8
>>> c.next()
16
>>> c.next()
32
>>> c.send(8)
64
>>> c.send(8)
128
>>> c.send(8)
256
Ahora, aquí está cómo haría el mismo tipo de función utilizando el envío, por lo que en cada iteración puede cambiar el valor del número:
def double_number(number):
while True:
number *= 2
number = yield number
Esto es lo que parece, ya que puede ver que al enviar un nuevo valor para el número cambia el resultado:
>>> def double_number(number):
... while True:
... number *= 2
... number = yield number
...
>>> c = double_number(4)
>>>
>>> c.send(None)
8
>>> c.send(5) #10
10
>>> c.send(1500) #3000
3000
>>> c.send(3) #6
6
También puede poner esto en un bucle for como ese:
for x in range(10):
n = c.send(n)
print n
Para obtener más ayuda, consulta este gran tutorial .
Estos me confundieron también. Aquí hay un ejemplo que hice al intentar configurar un generador que produce y acepta señales en orden alterno (rendimiento, aceptación, rendimiento, aceptación) ...
def echo_sound():
thing_to_say = ''<Sound of wind on cliffs>''
while True:
thing_to_say = (yield thing_to_say)
thing_to_say = ''...''.join([thing_to_say]+[thing_to_say[-6:]]*2)
yield None # This is the return value of send.
gen = echo_sound()
print ''You are lost in the wilderness, calling for help.''
print ''------''
in_message = gen.next()
print ''You hear: "{}"''.format(in_message)
out_message = ''Hello!''
print ''You yell "{}"''.format(out_message)
gen.send(out_message)
print ''------''
in_message = gen.next()
print ''You hear: "{}"''.format(in_message)
out_message = ''Is anybody out there?''
print ''You yell "{}"''.format(out_message)
gen.send(out_message)
print ''------''
in_message = gen.next()
print ''You hear: "{}"''.format(in_message)
out_message = ''Help!''
print ''You yell "{}"''.format(out_message)
gen.send(out_message)
El resultado es:
You are lost in the wilderness, calling for help.
------
You hear: "<Sound of wind on cliffs>"
You yell "Hello!"
------
You hear: "Hello!...Hello!...Hello!"
You yell "Is anybody out there?"
------
You hear: "Is anybody out there?...there?...there?"
You yell "Help!"
Se usa para enviar valores a un generador que acaba de ceder. Aquí hay un ejemplo explicativo artificial (no útil):
>>> def double_inputs():
... while True:
... x = yield
... yield x * 2
...
>>> gen = double_inputs()
>>> next(gen) # run up to the first yield
>>> gen.send(10) # goes into ''x'' variable
20
>>> next(gen) # run up to the next yield
>>> gen.send(6) # goes into ''x'' again
12
>>> next(gen) # run up to the next yield
>>> gen.send(94.3) # goes into ''x'' again
188.5999999999999
No puedes hacer esto solo con yield
.
En cuanto a por qué es útil, uno de los mejores casos de uso que he visto es Twisted''s @defer.inlineCallbacks
. Básicamente, le permite escribir una función como esta:
@defer.inlineCallbacks
def doStuff():
result = yield takesTwoSeconds()
nextResult = yield takesTenSeconds(result * 10)
defer.returnValue(nextResult / 10)
Lo que sucede es que takesTwoSeconds()
devuelve un Deferred
, que es un valor que promete que un valor se calculará más tarde. Twisted puede ejecutar el cálculo en otro hilo. Cuando finaliza el cálculo, lo pasa al diferido, y el valor luego se devuelve a la función doStuff()
. Por lo tanto, doStuff()
puede parecer más o menos como una función de procedimiento normal, excepto que puede hacer todo tipo de cálculos y devoluciones de llamada, etc. La alternativa anterior a esta funcionalidad sería hacer algo como:
def doStuff():
returnDeferred = defer.Deferred()
def gotNextResult(nextResult):
returnDeferred.callback(nextResult / 10)
def gotResult(result):
takesTenSeconds(result * 10).addCallback(gotNextResult)
takesTwoSeconds().addCallback(gotResult)
return returnDeferred
Es mucho más intrincado y difícil de manejar.