Generadores Asíncronos en Python

Introducción

Los generadores constituyen un componente fundamental en Python. Un ganerador es una función que contiene al menos una expresión yield. Estas funciones tienen la capacidad de pausarse y reanudarse, funcionando de manera similar a las corrutinas.

Las corrutinas de Python representan una extensión de los generadores tradicionales. El módulo Asyncio nos permite desarrollar generadores asíncronos mediante la definición de corrutinas que utilizan expresiones yield.

  1. Definición de Generadores Asíncronos

Un generador asíncrono es una corrutina que emplea expresiones yield. Antes de profundizar en sus detalles, es conveniente revisar los generadores clásicos de Python.

1.1. Generadores Tradicionales

Un generador tradicional es una función Python que retorna valores mediante la expresión yield.

# definir un generador tradicional
def secuencia_numeros():
    for numero in range(20):
        yield numero

La ejecución del generador se detiene en la expresión yield, devolviendo un valor y pausándose en ese punto. En la siguiente ejecución, el generador se reanuda desde el punto de pausa y continúa hasta la siguiente expresión yield.

Técnicamente, una función generadora crea y retorna un iterador de generador. Este iterador ejecuta el contenido de la función generadora, produciendo y recuperando valores según sea necesario.

El iterador puede ejecutarse paso a paso utilizadno la función integrada next().

...
# crear el generador
iterador = secuencia_numeros()
# avanzar un paso
valor = next(iterador)

Generalmente, se itera sobre el generador hasta completarlo, utilizando bucles for o comprensiones de listas.

...
# recorrer el generador y recopilar resultados
resultados = [elemento for elemento in secuencia_numeros()]

1.2. Generadores Asíncronos

Los generadores asíncronos son corrutinas que utilizan expresiones yield. A diferencia de los generadores tradicionales, las corrutinas pueden programar y esperar otras corrutinas y tareas.

Al igual que los generadores clásicos, las funciones generadoras asíncronas permiten crear iteradores asíncronos que pueden recorrerese utilizando la función integrada anext() en lugar de next().

Esto significa que los iteradores de generadores asíncronos implementan el método __anext__() y pueden utilizarse con expresiones async for.

Cada iteración del generador se programa y ejecuta como un objeto awaitable. La expresión async for programa y ejecuta cada iteración del generador, pausando la corrutina llamadora y esperando el resultado.

  1. Utilización de Generadores Asíncronos

En esta sección, examinaremos detalladamente cómo definir, crear, avanzar y recorrer generadores asíncronos en programas asyncio.

2.1. Definición

Definimos un generador asíncrono mediante una corrutina que contenga al menos una expresión yield. La función debe definirse utilizando la expresión async def.

# definir un generador asíncrono
async def generador_datos():
    for elemento in range(15):
        yield elemento * 2

Dado que un generador asíncrono es una corrutina y cada iterador retorna un objeto awaitable que se programa y ejecuta en el bucle de eventos de asyncio, podemos ejecutar y esperar objetos awaitable dentro del cuerpo del generador.

# definir un generador asíncrono con espera
async def generador_datos():
    for elemento in range(15):
        # pausar y esperar un momento
        await asyncio.sleep(0.5)
        #yield un valor al llamador
        yield elemento * 2

2.2. Creación

Para utilizar un generador asíncrono, debemos crearlo. Esto parece una llamada a función, pero en realidad crea y retorna un objeto iterador.

...
# crear el iterador
iterador = generador_datos()

Esto retorna un iterador asíncrono conocido como iterador de generador asíncrono.

2.3. Avance Paso a Paso

Podemos recorrer el generador un paso a la vez utilizando la función integrada anext(), de manera análoga a cómo se usa la función next() con generadores tradicionales.

El resultado es un objeto awaitable.

...
# obtener un awaitable para un paso del generador
objeto_espera = anext(iterador)
# ejecutar un paso del generador y obtener el resultado
valor = await objeto_espera

Esto puede realizarse en una sola línea.

...
# avanzar el generador asíncrono
valor = await anext(iterador)

2.4. Recorrido

También podemos recorrer generadores asíncronos en un bucle utilizando la expresión async for, que automáticamente esperará cada iteración del bucle.

...
# recorrer un generador asíncrono
async for valor in generador_datos():
    print(f"Dato recibido: {valor}")

Podemos utilizar comprensiones de listas asíncronas con la expresión async for para recopilar los resultados del generador.

...
# comprensión de lista asíncrona con generador asíncrono
resultados = [item async for item in generador_datos()]

  1. Ejemplo de Generador Asíncrono

Veamos cómo utilizar la expresión async for para recorrer un generador asíncrono.

En este ejemplo, utilizaremos un bucle async for para recorrer el generador hasta completarlo.

Este bucle esperará automáticamente cada objeto awaitable retornado por el generador, recuperando el valor producido y haciéndolo disponible dentro del cuerpo del bucle para su procesamiento.

Este es probablemente el patrón de uso más común para generadores asíncronos.

# Ejemplo de generador asíncrono con bucle async for
import asyncio

# definir un generador asíncrono
async def generador_datos():
    # bucle normal
    for índice in range(10):
        # bloquear simulando trabajo
        await asyncio.sleep(1)
        #yield el resultado
        yield índice

# corrutina principal
async def programa_principal():
    # iterar sobre el generador asíncrono con async for
    async for elemento in generador_datos():
        print(f"Procesando: {elemento}")

# ejecutar el programa asyncio
asyncio.run(programa_principal())

Al ejecutar el ejemplo, primero se crea la corrutina programa_principal() y se utiliza como punto de entrada del programa asyncio. La corrutina programa_principal() se ejecuta e inicia el bucle for.

Se crea una instancia del generador asíncrono, y el bucle utiliza internamente la función anext() para avanzar automáticamente un paso, retornando un objeto awaitable. Luego, el bucle espera el objeto awaitable y recupera un valor que se utiliza en el cuerpo del bucle para reportarlo.

Este proceso se repite, pausando la corrutina programa_principal(), ejecutando la iteración del generador, y nuevamente pausando y reanudando la corrutina hasta que el generador se agota.

Esto demuestra cómo utilizar la expresión async for para recorrer generadores asíncronos.

Procesando: 0
Procesando: 1
Procesando: 2
Procesando: 3
Procesando: 4
Procesando: 5
Procesando: 6
Procesando: 7
Procesando: 8
Procesando: 9

Etiquetas: Python asyncio async generators coroutines

Publicado el 6-1 19:30