with - yield instruction python
En la práctica, ¿cuáles son los usos principales de la nueva sintaxis de "rendimiento desde" en Python 3.3? (6)
Estoy teniendo dificultades para envolver mi cerebro alrededor de PEP 380 .
- ¿En qué situaciones es útil el "rendimiento de"?
- ¿Cuál es el caso de uso clásico?
- ¿Por qué se compara con los micro-hilos?
[actualizar]
Ahora entiendo la causa de mis dificultades. He usado generadores, pero nunca realmente usé coroutines (introducidos por PEP-342 ). A pesar de algunas similitudes, los generadores y las coroutinas son básicamente dos conceptos diferentes. Comprender las coroutinas (no solo los generadores) es la clave para entender la nueva sintaxis.
En mi humilde opinión, las funciones de Python son las más oscuras , la mayoría de los libros hacen que parezcan inútiles y poco interesantes.
Gracias por las excelentes respuestas, pero gracias especialmente a agf y su comentario que vincula las presentaciones de David Beazley . David se mece.
¿En qué situaciones es útil el "rendimiento de"?
Cada situación en la que tienes un bucle como este:
for x in subgenerator:
yield x
Como describe el PEP, este es un intento bastante ingenuo de usar el subgenerador, le faltan varios aspectos, especialmente el manejo adecuado de los .throw()
/ .send()
/ .close()
introducidos por el PEP-342 . Para hacer esto correctamente, es necesario un código bastante complicado .
¿Cuál es el caso de uso clásico?
Tenga en cuenta que desea extraer información de una estructura de datos recursiva. Digamos que queremos obtener todos los nodos de hoja en un árbol:
def traverse_tree(node):
if not node.children:
yield node
for child in node.children:
yield from traverse_tree(child)
Aún más importante es el hecho de que hasta el yield from
no existía un método simple para refactorizar el código del generador. Supongamos que tienes un generador (sin sentido) como este:
def get_list_values(lst):
for item in lst:
yield int(item)
for item in lst:
yield str(item)
for item in lst:
yield float(item)
Ahora decides dividir estos bucles en generadores separados. Sin yield from
, esto es feo, hasta el punto en el que pensará dos veces si realmente quiere hacerlo. Con el yield from
, es realmente agradable mirar:
def get_list_values(lst):
for sub in [get_list_values_as_int,
get_list_values_as_str,
get_list_values_as_float]:
yield from sub(lst)
¿Por qué se compara con los micro-hilos?
Creo que de lo que habla esta sección en el PEP es que cada generador tiene su propio contexto de ejecución aislado. Junto con el hecho de que la ejecución se conmuta entre el generador-iterador y la persona que llama utilizando el yield
y __next__()
, respectivamente, esto es similar a los hilos, donde el sistema operativo cambia el hilo en ejecución de vez en cuando, junto con el contexto de ejecución ( pila, registros, ...).
El efecto de esto también es comparable: tanto el generador-iterador como la persona que llama progresan en su estado de ejecución al mismo tiempo, sus ejecuciones están intercaladas. Por ejemplo, si el generador realiza algún tipo de cálculo y la persona que llama imprime los resultados, los verá tan pronto como estén disponibles. Esta es una forma de concurrencia.
Sin embargo, esa analogía no es algo específico de lo que yield from
, es más bien una propiedad general de los generadores en Python.
Donde sea que invoque un generador desde dentro de un generador, necesitará una "bomba" para for v in inner_generator: yield v
los valores: for v in inner_generator: yield v
. Como señala el PEP, hay sutiles complejidades en esto que la mayoría de las personas ignoran. El control de flujo no local, como throw()
es un ejemplo dado en el PEP. El nuevo yield from inner_generator
sintaxis yield from inner_generator
se usa en todos los lugares donde se hubiera escrito el bucle explícito for
antes. Sin embargo, no es simplemente azúcar sintáctica: maneja todos los casos de esquina que el bucle for
ignora. Ser "azucarado" alienta a las personas a usarlo y así obtener los comportamientos correctos.
Este mensaje en el hilo de discusión habla sobre estas complejidades:
Con las características adicionales del generador introducidas por PEP 342, ese ya no es el caso: como se describe en el PEP de Greg, la iteración simple no admite enviar () y lanzar () correctamente. La gimnasia necesaria para soportar el envío () y el lanzamiento () en realidad no son tan complejos cuando se rompen, pero tampoco son triviales.
No puedo hablar de una comparación con micro-hilos, aparte de observar que los generadores son un tipo de paralelismo. Puede considerar que el generador suspendido es un hilo que envía valores a través de yield
a un hilo consumidor. La implementación real puede no ser nada como esto (y la implementación real obviamente es de gran interés para los desarrolladores de Python) pero esto no concierne a los usuarios.
El nuevo yield from
sintaxis no agrega ninguna capacidad adicional al lenguaje en términos de subprocesos, solo facilita el uso correcto de las características existentes. O más precisamente, hace que sea más fácil para un consumidor novato de un generador interno complejo escrito por un experto pasar a través de ese generador sin romper ninguna de sus características complejas.
En el uso aplicado para la coroutina de E / S asíncrona , el yield from
tiene un comportamiento similar al de la await
en una función de coroutine . Ambos de los cuales se utilizan para suspender la ejecución de coroutine.
yield from
es utilizado por la corutina basada en generador .await
se utiliza paraasync def
coroutine. (desde Python 3.5+)
Para Asyncio, si no es necesario admitir una versión anterior de Python (es decir,> 3.5), async def
/ await
es la sintaxis recomendada para definir una coroutine. Por lo tanto, el yield from
ya no es necesario en una coroutina.
Pero, en general, fuera de asyncio, el yield from <sub-generator>
tiene todavía otro uso en la iteración del subgenerador como se mencionó en la respuesta anterior.
Un breve ejemplo le ayudará a comprender el yield from
caso de uso: obtener valor de otro generador
def flatten(sequence):
"""flatten a multi level list or something
>>> list(flatten([1, [2], 3]))
[1, 2, 3]
>>> list(flatten([1, [2], [3, [4]]]))
[1, 2, 3, 4]
"""
for element in sequence:
if hasattr(element, ''__iter__''):
yield from flatten(element)
else:
yield element
print(list(flatten([1, [2], [3, [4]]])))
yield from
cadenas básicamente iteradores de una manera eficiente:
# chain from itertools:
def chain(*iters):
for it in iters:
for item in it:
yield item
# with the new keyword
def chain(*iters):
for it in iters:
yield from it
Como puedes ver, elimina un bucle de Python puro. Eso es prácticamente todo lo que hace, pero encadenar iteradores es un patrón bastante común en Python.
Los hilos son básicamente una función que le permite saltar de funciones en puntos completamente aleatorios y volver al estado de otra función. El supervisor de subprocesos hace esto muy a menudo, por lo que el programa parece ejecutar todas estas funciones al mismo tiempo. El problema es que los puntos son aleatorios, por lo que debe usar el bloqueo para evitar que el supervisor detenga la función en un punto problemático.
Los generadores son muy parecidos a los hilos en este sentido: te permiten especificar puntos específicos (siempre que yield
) donde puedes saltar y entrar. Cuando se usan de esta manera, los generadores se llaman coroutines.
Lee estos excelentes tutoriales sobre coroutines en Python para más detalles.
Vamos a sacar una cosa del camino primero. La explicación que el yield from g
es equivalente a for v in g: yield v
ni siquiera comienza a hacer justicia a lo que se refiere el yield from
. Porque, seamos realistas, si todo el yield from
do es expandir el bucle for
, entonces no se garantiza que se agregue yield from
al lenguaje y se impide la implementación de un montón de nuevas características en Python 2.x.
De lo que se yield from
es que establece una conexión bidireccional transparente entre la persona que llama y el sub-generador :
La conexión es "transparente" en el sentido de que también propagará todo correctamente, no solo los elementos que se están generando (por ejemplo, las excepciones se propagan).
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, el yield from g
podría significar "ahora desconecte temporalmente el zócalo de mi cliente y vuelva a conectarlo a este otro zócalo del servidor" ) .
Por cierto, si no está seguro de lo que significa el envío de datos a un generador , debe eliminar todo y leer primero sobre coroutines ; son muy útiles (contrastarlos con subrutinas ), pero desafortunadamente, son menos conocidos en Python. El Curious Course on Couroutines de Dave Beazley es un excelente comienzo. Lea las diapositivas 24-33 para una introducción rápida.
Leyendo datos de un generador usando el 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 sobre reader()
, podemos yield from
él.
def reader_wrapper(g):
yield from g
Eso funciona, y eliminamos una línea de código. Y probablemente la intención sea un poco más clara (o no). Pero nada cambia la vida.
Envío de datos a un generador (coroutine) utilizando el rendimiento de - Parte 1
Ahora hagamos algo más interesante. Vamos a crear un writer
llamado coroutine que acepte los datos que se le envíen y los 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 envoltorio el envío de datos al escritor, de modo que cualquier información que se envíe al envoltorio se envíe de manera 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 envoltorio debe aceptar los datos que se le envían (obviamente) y también debe manejar la StopIteration
cuando se agota el bucle for. Evidentemente solo haciendo for x in coro: yield x
no funcionará. 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. ¡Mágico!
Envío de datos a un rendimiento del generador de - Parte 2 - Manejo de excepciones
Vamos a hacerlo más complicado. ¿Qué pasa si nuestro escritor necesita manejar excepciones? Digamos que el writer
maneja una SpamException
e 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 levanta la excepción y todo se detiene. Hagámoslo funcionar, pero manejando excepciones manualmente y enviándolas o enviándolas al sub-generador ( 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 esto también!
def writer_wrapper(coro):
yield from coro
El yield from
los mangos transparentes envía los valores o arroja valores al subgenerador.
Esto todavía no cubre todos los casos de la esquina sin embargo. ¿Qué pasa si el generador exterior está cerrado? ¿Qué ocurre con el caso en que el sub-generador devuelve un valor (sí, en Python 3.3+, los generadores pueden devolver valores), cómo se debe propagar el valor de retorno? El yield from
los mangos transparentes de todos los casos de esquina es realmente impresionante . yield from
tan solo funciona mágicamente y maneja todos esos casos.
Personalmente, siento que el yield from
es una mala elección de palabras clave porque no hace aparente la naturaleza bidireccional . Se propusieron otras palabras clave (como delegate
pero se rechazaron porque agregar una nueva palabra clave al idioma es mucho más difícil que combinar las existentes).
En resumen, es mejor pensar en el yield from
como un transparent two way channel
entre la persona que llama y el subgenerador.
Referencias: