En la práctica, ¿cuáles son los principales usos de la nueva sintaxis "yield from" en Python 3.3?


Estoy teniendo dificultades para envolver mi cerebro alrededor de PEP 380.

  1. ¿Cuáles son las situaciones en las que "yield from" es útil?
  2. ¿Cuál es el caso de uso clásico?
  3. ¿Por qué se compara con micro-hilos?

[ actualización ]

Ahora entiendo la causa de mis dificultades. He usado generadores, pero nunca he usado corrutinas (introducido por PEP-342). A pesar de algunas similitudes, los generadores y las corrutinas son básicamente dos concepto. Comprender las corrutinas (no solo los generadores) es la clave para comprender la nueva sintaxis.

EN mi humilde opinión las corrutinas son la característica más oscura de Python, la mayoría de los libros la hacen parecer inútil y poco interesante.

Gracias por las grandes respuestas, pero un agradecimiento especial a agf y su comentario enlazando a David Beazley presentations. David rocas.

 243
Author: Community, 2012-03-14

6 answers

Primero quitemos una cosa del camino. La explicación que yield from g es equivalente 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 bucle for, 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 establece una conexión bidireccional transparente entre el la persona que llama y el subgenerador:

  • La conexión es "transparente" en el sentido de que propagará todo correctamente también, no solo los elementos que se generan (por ejemplo, se propagan las excepciones).

  • La conexión es "bidireccional" en el sentido de que los datos pueden enviarse desde y a un generador.

(Si estuviéramos hablando de TCP, yield from g podría significar " ahora desconectar temporalmente mi socket del cliente y reconectarlo a este otro socket del servidor".)

Por cierto, si no está seguro de lo que significa enviar datos a un generador, primero debe dejar todo y leer sobre las corrutinas-son muy útiles (contrastarlas con subrutinas), pero desafortunadamente menos conocidas en Python. El Curioso Curso de Dave Beazley sobre Couroutines es un excelente comienzo. Lea las diapositivas 24-33 para una cartilla rápida.

Lectura datos de un generador utilizando 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 simplemente yield from.

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.

Enviar datos a un generador (corrutina) usando yield from-Parte 1

Ahora hagamos algo más interesante. Vamos a crear una corrutina llamada writer que acepta los datos enviados a ella y escribe a 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 la función wrapper manejar el envío de datos al escritor, de modo que cualquier dato que se envíe al wrapper sea transparente enviado 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 wrapper necesita aceptar los datos que se le envían (obviamente) y también debe manejar el StopIteration cuando se agote 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 este.

def writer_wrapper(coro):
    yield from coro

Eso guarda 6 líneas de código, lo hace mucho más legible y simplemente funciona. ¡Magia!

Envío de datos a un generador yield from-Parte 2-Manejo de excepciones

Hagámoslo más complicado. ¿Y si nuestro escritor necesita manejar excepciones? Digamos que el writer maneja un SpamException e imprime *** si encuentra uno.

class SpamException(Exception):
    pass

def writer():
    while True:
        try:
            w = (yield)
        except SpamException:
            print('***')
        else:
            print('>> ', w)

¿Y si no cambiamos writer_wrapper? Funciona? Vamos a intentar

# 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 {[29]]} solo levanta 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 esto también!

def writer_wrapper(coro):
    yield from coro

El yield from maneja de forma transparente el envío de los valores o el lanzamiento de valores en el sub-generador.

Esto todavía no cubre todos los casos de esquina sin embargo. ¿Qué sucede si el generador exterior está cerrado? ¿Qué pasa con en el caso de que el subgenerador devuelva un valor (sí, en Python 3.3+, los generadores pueden devolver valores), ¿cómo debe propagarse el valor devuelto? Que yield from maneje de forma transparente todas las cajas de esquina es realmente impresionante. yield from simplemente funciona mágicamente y maneja todos esos casos.

Personalmente siento que yield fromes una mala elección de palabra clave porque no hace aparente la naturaleza bidireccional. Hubo otras palabras clave propuestas (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, es mejor pensar en yield from como un transparent two way channel entre la persona que llama y el sub-generador.

Referencias:

  1. PEP 380 Sintaxis para delegar a una sub-generador (Ewing) [v3.3, 2009-02-13]
  2. PEP 342 - Corrutinas a través de generadores mejorados (GvR, Eby) [v2.5, 2005-05-10]
 341
Author: Praveen Gollakota,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-10-01 00:00:19

¿Cuáles son las situaciones en las que "yield from" es útil?

Cada situación donde 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 la .throw()/.send()/.close() mecanismos introducidos por PEP 342. Para hacer esto correctamente, es necesario un código bastante complicado.

¿Cuál es el uso clásico ¿case?

Considere que desea extraer información de una estructura de datos recursiva. Digamos que queremos obtener todos los nodos de la 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 había un método simple de refactorizar el código del generador. Supongamos que usted tiene 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 factorizar estos bucles en generadores separados. Sin yield from, esto es feo, hasta el punto en que pensarás dos veces si realmente quieres hacerlo. Con yield from, es realmente agradable de ver:

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 micro-hilos?

Creo que de lo que esta sección en el PEP está hablando es de que cada generador tiene su propio contexto de ejecución aislado. Junto con el hecho de que la ejecución se cambia entre el generador-iterador y la persona que llama usando yield y __next__(), respectivamente, esto es similar a los subprocesos, donde el sistema operativo cambia el subproceso de 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 el llamador progresan en su estado de ejecución al mismo tiempo, sus ejecuciones están intercaladas. Por ejemplo, si el generador hace algún tipo de cálculo y la persona que llama imprime los resultados, verá los resultados tan pronto como estén disponibles. Esta es una forma de concurrencia.

Esa analogía sin embargo, no es nada específico de yield from - es más bien una propiedad general de los generadores en Python.

 72
Author: Niklas B.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-07-04 10:31:13

Donde quiera que invoque un generador desde dentro de un generador, necesita una "bomba" para re-yield los valores: for v in inner_generator: yield v. Como señala el PEP, hay complejidades sutiles en esto que la mayoría de la gente ignora. El control de flujo no local como throw() es un ejemplo dado en el PEP. La nueva sintaxis yield from inner_generator se usa siempre que haya escrito el bucle explícito for antes. Sin embargo, no es simplemente azúcar sintáctica: Maneja todos los casos de esquina que son ignorados por el bucle for. Ser "azucarado" anima a las personas a usarlo y así obtener los comportamientos correctos.

Este mensaje en el hilo de discusión habla de estas complejidades:

Con las características adicionales del generador introducidas por PEP 342, eso no es más largo el caso: como se describe en el PEP de Greg, iteración simple no soporta send() y throw () correctamente. La gimnasia necesaria para apoyar send () y throw() en realidad no son tan complejos cuando los rompes abajo, pero no son triviales bien.

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 subproceso que envía valores a través de yield a un subproceso de consumo. La implementación real puede no ser nada como esto (y la implementación real es obviamente de gran interés para los desarrolladores de Python), pero esto no concierne a los usuarios.

La nueva sintaxis yield from no añade ningún elemento adicional capacidad para el lenguaje en términos de enhebrado, solo hace que sea más fácil usar las características existentes correctamente. O más precisamente, hace que sea más fácil para un novato consumidor de un generador interno complejo escrito por un experto pasar a través de ese generador sin romper ninguna de sus características complejas.

 27
Author: Ben Jackson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-03-14 19:58:51

Un breve ejemplo le ayudará a entender uno de los casos de uso de yield from: 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]]])))
 13
Author: ospider,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-01-19 04:58:32

yield from básicamente encadena 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 Python puro. Eso es casi todo lo que hace, pero encadenar iteradores es un patrón bastante común en Python.

Los hilos son básicamente una característica que le permite saltar fuera 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 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 bastante similares a los hilos en este sentido: Te permiten especificar puntos específicos (siempre que yield) donde puedes entrar y salir. Cuando se usa de esta manera, los generadores se llaman corrutinas.

Lee estos excelentes tutoriales sobre corrutinas en Python para más detalles

 3
Author: Jochen Ritzel,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-03-14 20:02:48

En el uso aplicado para la coroutina de IO asíncrona , yield from tiene un comportamiento similar a await en una función de coroutina . Ambos se utilizan para suspender la ejecución de la corrutina.

  • yield from es utilizado por la corrutina basada en generadores .

  • await se utiliza para async def corutina. (desde Python 3.5+)

Para Asyncio, si no hay necesidad de soportar una versión anterior de Python (i. e. >3.5), async def/await es la sintaxis recomendada para definir una corrutina. Por lo tanto, yield from ya no es necesario en una corrutina.

Pero en general fuera de asyncio, yield from <sub-generator> todavía tiene algún otro uso en la iteración del subgenerador como se mencionó en la respuesta anterior.

 1
Author: Yeo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-08-26 21:05:08