La palabra "ceder" tiene dos significados: producir algo (p. Ej., Ceder maíz) y detenerse para permitir que alguien / otra cosa continúe (p. Ej., Autos que ceden el paso a los peatones). Ambas definiciones se aplican a la yieldpalabra clave de Python ; Lo que hace que las funciones del generador sean especiales es que, a diferencia de las funciones regulares, los valores pueden ser "devueltos" a la persona que llama mientras simplemente detiene, no termina, una función del generador.
Es más fácil imaginar un generador como un extremo de una tubería bidireccional con un extremo "izquierdo" y un extremo "derecho"; esta tubería es el medio sobre el cual se envían valores entre el generador mismo y el cuerpo de la función del generador. Cada extremo de la tubería tiene dos operaciones:push que envía un valor y bloquea hasta que el otro extremo de la tubería extrae el valor y no devuelve nada; ypull, que bloquea hasta que el otro extremo de la tubería empuja un valor y devuelve el valor empujado. En tiempo de ejecución, la ejecución rebota de un lado a otro entre los contextos a cada lado de la tubería: cada lado se ejecuta hasta que envía un valor al otro lado, en ese punto se detiene, deja que el otro lado se ejecute y espera un valor en volver, en cuyo punto el otro lado se detiene y se reanuda. En otras palabras, cada extremo de la tubería se ejecuta desde el momento en que recibe un valor hasta el momento en que envía un valor.
La tubería es funcionalmente simétrica, pero, por convención que estoy definiendo en esta respuesta, el extremo izquierdo solo está disponible dentro del cuerpo de la función del generador y es accesible a través de la yieldpalabra clave, mientras que el extremo derecho es el generador y es accesible a través del sendFunción del generador . Como interfaces singulares a sus respectivos extremos de la tubería, yieldy sendcumplen una doble función: cada uno empuja y tira valores hacia / desde sus extremos de la tubería, yieldempujando hacia la derecha y hacia la izquierda mientras sendhace lo contrario. Este doble deber es el quid de la confusión que rodea la semántica de declaraciones como x = yield y. Romper yieldy senddescender en dos pasos explícitos de empujar / tirar hará que su semántica sea mucho más clara:
- Supongamos que
ges el generador. g.sendempuja un valor hacia la izquierda a través del extremo derecho de la tubería.
- Ejecución dentro del contexto de
gpausas, permitiendo que el cuerpo de la función del generador se ejecute.
- El valor empujado por
g.sendes arrastrado hacia la izquierda yieldy recibido en el extremo izquierdo de la tubería. En x = yield y, xse asigna al valor extraído.
- La ejecución continúa dentro del cuerpo de la función del generador hasta que
yieldse alcanza la siguiente línea que contiene .
yieldempuja un valor hacia la derecha a través del extremo izquierdo de la tubería, hacia arriba g.send. En x = yield y, yse empuja hacia la derecha a través de la tubería.
- La ejecución dentro del cuerpo de la función del generador se detiene, permitiendo que el alcance externo continúe donde lo dejó.
g.send reanuda y extrae el valor y lo devuelve al usuario.
- La
g.sendpróxima vez que se llame, regrese al Paso 1.
Si bien es cíclico, este procedimiento tiene un comienzo: cuándo g.send(None), que es lo que next(g)es la abreviatura, se llama por primera vez (es ilegal pasar algo que no sea Nonea la primera sendllamada). Y puede tener un final: cuando no hay más yielddeclaraciones que alcanzar en el cuerpo de la función del generador.
¿Ves lo que hace que la yielddeclaración (o más exactamente, los generadores) sea tan especial? A diferencia de la returnpalabra clave miserable , yieldpuede pasar valores a su interlocutor y recibir valores de su interlocutor, ¡todo sin terminar la función en la que vive! (Por supuesto, si desea terminar una función, o un generador, también es útil tener la returnpalabra clave). Cuando yieldse encuentra una declaración, la función del generador simplemente hace una pausa y luego vuelve a subir justo donde dejó apagado al ser enviado otro valor. Y sendes solo la interfaz para comunicarse con el interior de una función generadora desde el exterior.
Si realmente queremos romper esta analogía de empujar / tirar / tubería lo más que podamos, terminamos con el siguiente pseudocódigo que realmente conduce a casa que, aparte de los pasos 1-5, yieldy sendson dos lados del mismo tubo de monedas :
right_end.push(None) # the first half of g.send; sending None is what starts a generator
right_end.pause()
left_end.start()
initial_value = left_end.pull()
if initial_value is not None: raise TypeError("can't send non-None value to a just-started generator")
left_end.do_stuff()
left_end.push(y) # the first half of yield
left_end.pause()
right_end.resume()
value1 = right_end.pull() # the second half of g.send
right_end.do_stuff()
right_end.push(value2) # the first half of g.send (again, but with a different value)
right_end.pause()
left_end.resume()
x = left_end.pull() # the second half of yield
goto 6
La transformación clave es que nos hemos dividido x = yield yy value1 = g.send(value2)cada uno en dos declaraciones: left_end.push(y)y x = left_end.pull(); y value1 = right_end.pull()y right_end.push(value2). Hay dos casos especiales de la yieldpalabra clave: x = yieldy yield y. Estos son azúcares sintácticos, respectivamente, para x = yield Noney _ = yield y # discarding value.
Para obtener detalles específicos sobre el orden preciso en que se envían los valores a través de la tubería, consulte a continuación.
Lo que sigue es un modelo concreto bastante largo de lo anterior. Primero, primero se debe tener en cuenta que para cualquier generador g, next(g)es exactamente equivalente a g.send(None). Con esto en mente, podemos centrarnos solo en cómo sendfunciona y hablar solo sobre el avance del generador send.
Supongamos que tenemos
def f(y): # This is the "generator function" referenced above
while True:
x = yield y
y = x
g = f(1)
g.send(None) # yields 1
g.send(2) # yields 2
Ahora, la definición de faproximadamente desugar a la siguiente función ordinaria (sin generador):
def f(y):
bidirectional_pipe = BidirectionalPipe()
left_end = bidirectional_pipe.left_end
right_end = bidirectional_pipe.right_end
def impl():
initial_value = left_end.pull()
if initial_value is not None:
raise TypeError(
"can't send non-None value to a just-started generator"
)
while True:
left_end.push(y)
x = left_end.pull()
y = x
def send(value):
right_end.push(value)
return right_end.pull()
right_end.send = send
# This isn't real Python; normally, returning exits the function. But
# pretend that it's possible to return a value from a function and then
# continue execution -- this is exactly the problem that generators were
# designed to solve!
return right_end
impl()
Lo siguiente ha sucedido en esta transformación de f:
- Hemos trasladado la implementación a una función anidada.
- Hemos creado una tubería bidireccional cuya
left_end accederá la función anidada y cuyo right_endalcance externo devolverá y accederá, right_endes lo que conocemos como el objeto generador.
- Dentro de la función anidada, la primera cosa que hacemos es comprobar que
left_end.pull()es None, consumiendo un valor empujado en el proceso.
- Dentro de la función anidada, la instrucción
x = yield yha sido reemplazada por dos líneas:left_end.push(y) y x = left_end.pull().
- Hemos definido la
sendfunción para right_end, que es la contraparte de las dos líneas que reemplazamosx = yield y declaración en el paso anterior.
En este mundo de fantasía donde las funciones pueden continuar después de regresar, gse asigna right_endy luego impl()se llama. Entonces, en nuestro ejemplo anterior, si siguiéramos la ejecución línea por línea, lo que sucedería es más o menos lo siguiente:
left_end = bidirectional_pipe.left_end
right_end = bidirectional_pipe.right_end
y = 1 # from g = f(1)
# None pushed by first half of g.send(None)
right_end.push(None)
# The above push blocks, so the outer scope halts and lets `f` run until
# *it* blocks
# Receive the pushed value, None
initial_value = left_end.pull()
if initial_value is not None: # ok, `g` sent None
raise TypeError(
"can't send non-None value to a just-started generator"
)
left_end.push(y)
# The above line blocks, so `f` pauses and g.send picks up where it left off
# y, aka 1, is pulled by right_end and returned by `g.send(None)`
right_end.pull()
# Rinse and repeat
# 2 pushed by first half of g.send(2)
right_end.push(2)
# Once again the above blocks, so g.send (the outer scope) halts and `f` resumes
# Receive the pushed value, 2
x = left_end.pull()
y = x # y == x == 2
left_end.push(y)
# The above line blocks, so `f` pauses and g.send(2) picks up where it left off
# y, aka 2, is pulled by right_end and returned to the outer scope
right_end.pull()
x = left_end.pull()
# blocks until the next call to g.send
Esto se asigna exactamente al pseudocódigo de 16 pasos anterior.
Hay algunos otros detalles, como cómo se propagan los errores y qué sucede cuando llega al final del generador (la tubería está cerrada), pero esto debería aclarar cómo funciona el flujo de control básico cuando send se usa.
Usando estas mismas reglas de desagüe, veamos dos casos especiales:
def f1(x):
while True:
x = yield x
def f2(): # No parameter
while True:
x = yield x
En su mayor parte, se desugaron de la misma manera que f, las únicas diferencias son cómo yieldse transforman las declaraciones:
def f1(x):
# ... set up pipe
def impl():
# ... check that initial sent value is None
while True:
left_end.push(x)
x = left_end.pull()
# ... set up right_end
def f2():
# ... set up pipe
def impl():
# ... check that initial sent value is None
while True:
left_end.push(x)
x = left_end.pull()
# ... set up right_end
En el primero, el valor pasado a f1se empuja (cede) inicialmente, y luego todos los valores extraídos (enviados) se empujan (ceden) de regreso. En el segundo, xno tiene ningún valor (todavía) cuando llega por primera vez push, por lo que UnboundLocalErrorse eleva un.