working with type outside instruction generators explicacion creating python yield

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 .

  1. ¿En qué situaciones es útil el "rendimiento de"?
  2. ¿Cuál es el caso de uso clásico?
  3. ¿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.

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:

  1. PEP 380 - Sintaxis para delegar a un subgenerador (Ewing) [v3.3, 2009-02-13]
  2. PEP-342 - Coroutines a través de Generadores Mejorados (GvR, Eby) [v2.5, 2005-05-10]