¿Cuál es el propósito de la función "enviar" en los generadores de Python?
Entiendo yield
. send
Pero, ¿qué hace la función de un generador ? La documentación dice:
generator.send(value)
Reanuda la ejecución y "envía" un valor a la función generadora. El
value
argumento se convierte en el resultado de layield
expresión actual. Elsend()
método devuelve el siguiente valor generado por el generador, o aumentaStopIteration
si el generador sale sin generar otro valor.
¿Qué significa eso? Pensé value
que era la entrada a la función del generador. La frase "El send()
método devuelve el siguiente valor arrojado por el generador" parece ser también el propósito exacto de yield
, que también devuelve el siguiente valor arrojado por el generador.
¿Existe un ejemplo de un generador que utiliza send
algo que yield
no puede lograr?
Se utiliza para enviar valores a un generador que acaba de rendir. 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 el de Twisted @defer.inlineCallbacks
. Básicamente, te 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 a Deferred
, que es un valor que promete que se calculará un valor más adelante. Twisted puede ejecutar el cálculo en otro hilo. Cuando finaliza el cálculo, lo pasa al diferido y luego el valor se envía de regreso a la doStuff()
función. Por lo tanto, doStuff()
puede terminar pareciéndose más o menos a una función de procedimiento normal, excepto que puede realizar todo tipo de cálculos y devoluciones de llamadas, etc. La alternativa antes de 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 complicado y difícil de manejar.
Esta función es para escribir corrutinas.
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 el control se pasa de un lado a otro? Esas son corrutinas. Se pueden usar para todo tipo de cosas interesantes como Asynch IO y similares.
Piénselo así, con un generador y sin envío, es una calle de sentido único
========== yield ========
Generator | ------------> | User |
========== ========
Pero con enviar, se convierte en una vía de doble sentido.
========== 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 numérico al crear instancias 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 así es como harías el mismo tipo de función usando enviar, de modo que en cada iteración puedas cambiar el valor del número:
def double_number(number):
while True:
number *= 2
number = yield number
Así es como se ve, como puede ver, 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 puedes poner esto en un bucle for como tal:
for x in range(10):
n = c.send(n)
print n
Para obtener más ayuda, consulte este fantástico tutorial .
El send()
método controla cuál será el valor a la izquierda de la expresión de rendimiento.
Para comprender en qué se diferencia el rendimiento y qué valor tiene, primero actualicemos rápidamente el orden en que se evalúa el código Python.
Sección 6.15 Orden de evaluación
Python evalúa expresiones de izquierda a derecha. Observe que al evaluar una tarea, el lado derecho se evalúa antes que el lado izquierdo.
Entonces, una expresión a = b
del lado derecho se evalúa primero.
Como lo demuestra lo siguiente, a[p('left')] = p('right')
el lado derecho se evalúa primero.
>>> def p(side):
... print(side)
... return 0
...
>>> a[p('left')] = p('right')
right
left
>>>
>>>
>>> [p('left'), p('right')]
left
right
[0, 0]
¿Qué hace el rendimiento?, produce, suspende la ejecución de la función y regresa a la persona que llama, y reanuda la ejecución en el mismo lugar donde la dejó antes de suspender.
¿Dónde exactamente se suspende la ejecución? Quizás ya lo hayas adivinado... la ejecución se suspende entre el lado derecho e izquierdo de la expresión de rendimiento. Entonces, new_val = yield old_val
la ejecución se detiene en la =
señal, y el valor de la derecha (que es antes de suspender, y también es el valor devuelto a la persona que llama) puede ser algo diferente que el valor de la izquierda (que es el valor que se asigna después de reanudar ejecución).
yield
produce 2 valores, uno a la derecha y otro a la izquierda.
¿Cómo se controla el valor del lado izquierdo de la expresión de rendimiento? a través del .send()
método.
6.2.9. Expresiones de rendimiento
El valor de la expresión de rendimiento después de la reanudación depende del método que reanudó la ejecución. Si
__next__()
se usa (normalmente a través de un for o elnext()
incorporado), el resultado es Ninguno. De lo contrario, sisend()
se usa, el resultado será el valor pasado a ese método.