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

407

Me está costando mucho asimilar PEP 380 .

  1. ¿En qué situaciones es útil "rendir de"?
  2. ¿Cuál es el caso de uso clásico?
  3. ¿Por qué se compara con microhilos?

[actualización]

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

Las corutinas de la OMI son la característica más oscura de Python , la mayoría de los libros hacen que parezca inútil y poco interesante.

Gracias por las excelentes respuestas, pero gracias especiales a agf y su comentario que vincula a las presentaciones de David Beazley . David rocas.

Paulo Scardine
fuente
77
Video de la presentación de dabeaz.com/coroutines de David Beazley : youtube.com/watch?v=Z_OAlIhXziw
jcugat

Respuestas:

572

Primero saquemos una cosa del camino. La explicación que yield from gequivale a for v in g: yield v ni siquiera comienza a hacer justicia a lo que yield fromse trata. Porque, seamos sinceros, si todo lo que yield fromhace es expandir el forciclo, entonces no garantiza agregar yield fromal lenguaje e impide que se implementen un montón de nuevas características en Python 2.x.

Lo que yield fromhace 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 gpodrí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 fromhacerlo.

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 writerque 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 StopIterationcuando se agota el bucle for. Evidentemente, solo hacer for x in coro: yield xno 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 writermaneja ay SpamExceptionse 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 fromtransparente 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 frommaneja de manera transparente todas las esquinas es realmente impresionante . yield fromsimplemente mágicamente funciona y maneja todos esos casos.

Personalmente, creo que yield fromes una mala elección de palabras clave porque no hace evidente la naturaleza bidireccional . Se propusieron otras palabras clave (como delegatepero 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 fromcomo transparent two way channelentre la persona que llama y el sub-generador.

Referencias

  1. PEP 380 - Sintaxis para delegar a un subgenerador (Ewing) [v3.3, 2009-02-13]
  2. PEP 342 - Corutinas a través de generadores mejorados (GvR, Eby) [v2.5, 2005-05-10]
Praveen Gollakota
fuente
3
@PraveenGollakota, en la segunda parte de su pregunta, Enviando datos a un generador (corutina) usando el rendimiento de - Parte 1 , ¿qué sucede si tiene más que corutinas para reenviar el artículo recibido? ¿Como un escenario de emisor o suscriptor en el que proporciona múltiples corutinas al contenedor en su ejemplo y los elementos deben enviarse a todos o a un subconjunto de ellos?
Kevin Ghaboosi
3
@PraveenGollakota, Felicitaciones por la gran respuesta. Los pequeños ejemplos me permiten probar cosas en la respuesta. ¡El enlace al curso de Dave Beazley fue un extra!
BiGYaN
1
hacer except StopIteration: passDENTRO del while True:bucle no es una representación precisa de yield from coro- que no es un bucle infinito y después de que corose agota (es decir, plantea StopIteration), writer_wrapperejecutará la siguiente instrucción. Después de la última declaración, se elevará automáticamente StopIterationcomo cualquier generador agotado ...
Aprillion
1
... así que si está writercontenido en for _ in range(4)lugar de while True, luego de imprimirlo >> 3, TAMBIÉN se elevará automáticamente StopIterationy esto se manejará automáticamente yield fromy luego writer_wrapperse elevará automáticamente StopIterationy, como wrap.send(i)no está dentro del trybloque, en realidad se elevará en este punto ( es decir, el rastreo solo informará la línea con wrap.send(i), no nada desde el interior del generador)
Aprillion
3
Al leer " ni siquiera comienza a hacer justicia ", sé que he llegado a la respuesta correcta. Gracias por la gran explicación!
Hot.PxL
89

¿En qué situaciones es útil "rendir de"?

Cada situación en la que tienes un bucle como este:

for x in subgenerator:
  yield x

Como describe la PEP, este es un intento bastante ingenuo de usar el subgenerador, le faltan varios aspectos, especialmente el manejo adecuado de los mecanismos .throw()/ .send()/ .close()introducidos por la 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 momento yield fromno había un método simple para refactorizar el código del generador. Supongamos que 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 fromesto, esto es feo, hasta el punto en que pensarás dos veces si realmente quieres hacerlo. Con yield from, en realidad es 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 microhilos?

Creo que de lo que habla esta sección del PEP es 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 yieldy __next__(), respectivamente, esto es similar a los hilos, donde el sistema operativo cambia el hilo 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, los verá tan pronto como estén disponibles. Esta es una forma de concurrencia.

Sin yield fromembargo, esa analogía no es algo específico : es más bien una propiedad general de los generadores en Python.

Niklas B.
fuente
Refactorizar generadores es doloroso hoy.
Josh Lee
1
Tiendo a usar itertools mucho para refactorizar generadores (cosas como itertools.chain), no es gran cosa. Me gusta el rendimiento, pero todavía no veo cuán revolucionario es. Probablemente lo sea, ya que Guido está loco por eso, pero debo estar perdiendo el panorama general. Supongo que es genial para send () ya que es difícil refactorizarlo, pero no lo uso con mucha frecuencia.
e-satis
Supongo que esos get_list_values_as_xxxson generadores simples con una sola línea for x in input_param: yield int(x)y los otros dos respectivamente con stryfloat
madtyn
@NiklasB. re "extraer información de una estructura de datos recursiva". Estoy llegando a Py para obtener datos. ¿Podrías apuñalar esta Q ?
alancalvitti
33

Dondequiera que se invoca un generador dentro de un generador necesita una "bomba" para volver a yieldlos valores: for v in inner_generator: yield v. Como señala el PEP, existen complejidades sutiles que la mayoría de la gente ignora. El control de flujo no local throw()es un ejemplo dado en el PEP. La nueva sintaxis yield from inner_generatorse usa donde haya escrito forantes el ciclo explícito . Sin embargo, no es simplemente azúcar sintáctico: maneja todos los casos de esquina que el forbucle 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 send () y throw () en realidad no es tan compleja cuando los desglosas, pero tampoco son triviales.

No puedo hablar de una comparación con microhilos, 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 yieldde un subproceso de 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.

La nueva yield fromsintaxis no agrega ninguna capacidad adicional al lenguaje en términos de subprocesos, simplemente facilita el uso correcto de las funciones existentes. O, más precisamente, facilita que un consumidor novato de un generador interno complejo escrito por un experto pase por ese generador sin romper ninguna de sus características complejas.

Ben Jackson
fuente
23

Un breve ejemplo lo ayudará a comprender uno de yield fromlos casos 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]]])))
ospider
fuente
2
Solo quería sugerir que la impresión al final se vería un poco mejor sin la conversión a una listaprint(*flatten([1, [2], [3, [4]]]))
yoniLavi
6

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

Los subprocesos son básicamente una característica 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 bastante similares a los hilos en este sentido: le permiten especificar puntos específicos (siempre que sean yield) donde puede saltar dentro y fuera. Cuando se usan de esta manera, los generadores se llaman corutinas.

Lea estos excelentes tutoriales sobre corutinas en Python para obtener más detalles.

Jochen Ritzel
fuente
10
Esta respuesta es engañosa porque elude la característica sobresaliente de "rendimiento de", como se mencionó anteriormente: soporte send () y throw ().
Justin W
2
@Justin W: Creo que lo que lee antes de que realmente se induce a error, ya que no le dieron el punto de que throw()/send()/close()son yieldcaracterísticas que yield from, evidentemente, ha de aplicar correctamente ya que se supone que es simplificar el código. Tales trivialidades no tienen nada que ver con el uso.
Jochen Ritzel
55
¿Estás disputando la respuesta de Ben Jackson anterior? Mi lectura de su respuesta es que es esencialmente un azúcar sintáctico que sigue la transformación del código que proporcionó. La respuesta de Ben Jackson específicamente refuta esa afirmación.
Justin W
@JochenRitzel Nunca necesita escribir su propia chainfunción porque itertools.chainya existe. Uso yield from itertools.chain(*iters).
Acumenus
5

En el uso aplicado para la rutina de E / S asíncrona , yield fromtiene un comportamiento similar al awaitde una función de rutina . Ambos se utilizan para suspender la ejecución de la rutina.

Para Asyncio, si no es necesario admitir una versión anterior de Python (es decir,> 3.5), async def/ awaites la sintaxis recomendada para definir una rutina. Por yield fromlo tanto, ya no es necesario en una rutina.

Sin embargo, en las afueras general de asyncio, yield from <sub-generator>tiene todavía algún otro uso en la iteración de la sub-generador como se menciona en la respuesta anterior.

Yeo
fuente
1

Este código define una función que fixed_sum_digitsdevuelve un generador que enumera los números de seis dígitos de modo que la suma de dígitos sea 20.

def iter_fun(sum, deepness, myString, Total):
    if deepness == 0:
        if sum == Total:
            yield myString
    else:  
        for i in range(min(10, Total - sum + 1)):
            yield from iter_fun(sum + i,deepness - 1,myString + str(i),Total)

def fixed_sum_digits(digits, Tot):
    return iter_fun(0,digits,"",Tot) 

Intenta escribirlo sin yield from. Si encuentra una manera efectiva de hacerlo, hágamelo saber.

Creo que para casos como este: visitar árboles, yield fromhace que el código sea más simple y limpio.

jimifiki
fuente