Primero saquemos una cosa del camino. La explicación que yield from g
equivale a for v in g: yield v
ni siquiera comienza a hacer justicia a lo que yield from
se trata. Porque, seamos sinceros, si todo lo que yield from
hace es expandir el for
ciclo, entonces no garantiza agregar yield from
al lenguaje e impide que se implementen un montón de nuevas características en Python 2.x.
Lo que yield from
hace es establecer una conexión bidireccional transparente entre la persona que llama y el subgenerador :
La conexión es "transparente" en el sentido de que también propagará todo correctamente, no solo los elementos que se generan (por ejemplo, se propagan excepciones).
La conexión es "bidireccional" en el sentido de que los datos pueden enviarse desde y hacia un generador.
( Si estuviéramos hablando de TCP, yield from g
podría significar "ahora desconecte temporalmente el socket de mi cliente y vuelva a conectarlo a este otro socket de servidor" ) .
Por cierto, si no está seguro de lo que significa enviar datos a un generador , primero debe descartar todo y leer sobre las rutinas ; son muy útiles (contrastarlas con las subrutinas ), pero desafortunadamente menos conocidas en Python. El curioso curso de Dave Beazley sobre Coroutines es un excelente comienzo. Lea las diapositivas 24-33 para obtener una introducción rápida.
Lectura de datos de un generador usando rendimiento de
def reader():
"""A generator that fakes a read from a file, socket, etc."""
for i in range(4):
yield '<< %s' % i
def reader_wrapper(g):
# Manually iterate over data produced by reader
for v in g:
yield v
wrap = reader_wrapper(reader())
for i in wrap:
print(i)
# Result
<< 0
<< 1
<< 2
<< 3
En lugar de iterar manualmente reader()
, podemos simplemente yield from
hacerlo.
def reader_wrapper(g):
yield from g
Eso funciona, y eliminamos una línea de código. Y probablemente la intención es un poco más clara (o no). Pero nada cambia la vida.
Envío de datos a un generador (corutina) usando el rendimiento de - Parte 1
Ahora hagamos algo más interesante. Creemos una rutina llamada writer
que acepte los datos que se le envíen y escriba en un socket, fd, etc.
def writer():
"""A coroutine that writes data *sent* to it to fd, socket, etc."""
while True:
w = (yield)
print('>> ', w)
Ahora la pregunta es, ¿cómo debe manejar la función de contenedor enviar datos al escritor, de modo que cualquier información que se envíe al contenedor se envíe de forma transparente al writer()
?
def writer_wrapper(coro):
# TBD
pass
w = writer()
wrap = writer_wrapper(w)
wrap.send(None) # "prime" the coroutine
for i in range(4):
wrap.send(i)
# Expected result
>> 0
>> 1
>> 2
>> 3
El reiniciador debe aceptar los datos que se le envían (obviamente) y también debe manejar StopIteration
cuando se agota el bucle for. Evidentemente, solo hacer for x in coro: yield x
no servirá. Aquí hay una versión que funciona.
def writer_wrapper(coro):
coro.send(None) # prime the coro
while True:
try:
x = (yield) # Capture the value that's sent
coro.send(x) # and pass it to the writer
except StopIteration:
pass
O podríamos hacer esto.
def writer_wrapper(coro):
yield from coro
Eso ahorra 6 líneas de código, lo hace mucho más legible y simplemente funciona. ¡Magia!
Envío de datos a un generador desde - Parte 2 - Manejo de excepciones
Hagámoslo más complicado. ¿Qué pasa si nuestro escritor necesita manejar excepciones? Digamos que writer
maneja ay SpamException
se imprime ***
si encuentra uno.
class SpamException(Exception):
pass
def writer():
while True:
try:
w = (yield)
except SpamException:
print('***')
else:
print('>> ', w)
¿Qué pasa si no cambiamos writer_wrapper
? ¿Funciona? Intentemos
# writer_wrapper same as above
w = writer()
wrap = writer_wrapper(w)
wrap.send(None) # "prime" the coroutine
for i in [0, 1, 2, 'spam', 4]:
if i == 'spam':
wrap.throw(SpamException)
else:
wrap.send(i)
# Expected Result
>> 0
>> 1
>> 2
***
>> 4
# Actual Result
>> 0
>> 1
>> 2
Traceback (most recent call last):
... redacted ...
File ... in writer_wrapper
x = (yield)
__main__.SpamException
Um, no está funcionando porque x = (yield)
solo aumenta la excepción y todo se detiene. Hagamos que funcione, pero manejando manualmente las excepciones y enviándolas o arrojándolas al subgenerador ( writer
)
def writer_wrapper(coro):
"""Works. Manually catches exceptions and throws them"""
coro.send(None) # prime the coro
while True:
try:
try:
x = (yield)
except Exception as e: # This catches the SpamException
coro.throw(e)
else:
coro.send(x)
except StopIteration:
pass
Esto funciona.
# Result
>> 0
>> 1
>> 2
***
>> 4
¡Pero eso también!
def writer_wrapper(coro):
yield from coro
El yield from
transparente se encarga de enviar los valores o arrojar valores al subgenerador.
Sin embargo, esto todavía no cubre todos los casos de esquina. ¿Qué sucede si el generador externo está cerrado? ¿Qué pasa con el caso cuando el subgenerador devuelve un valor (sí, en Python 3.3+, los generadores pueden devolver valores), ¿cómo debe propagarse el valor devuelto? Eso yield from
maneja de manera transparente todas las esquinas es realmente impresionante . yield from
simplemente mágicamente funciona y maneja todos esos casos.
Personalmente, creo que yield from
es una mala elección de palabras clave porque no hace evidente la naturaleza bidireccional . Se propusieron otras palabras clave (como delegate
pero fueron rechazadas porque agregar una nueva palabra clave al idioma es mucho más difícil que combinar las existentes).
En resumen, lo mejor es pensar en yield from
como transparent two way channel
entre la persona que llama y el sub-generador.
Referencias
- PEP 380 - Sintaxis para delegar a un subgenerador (Ewing) [v3.3, 2009-02-13]
- PEP 342 - Corutinas a través de generadores mejorados (GvR, Eby) [v2.5, 2005-05-10]