contextlib.aclosing: cierra generadores async

Publicado el: 30/08/2026
Tempo de leitura: 5 minutos
Vivid close-up of a python resting among autumn leaves, showcasing its intricate patterns.

Los generadores asíncronos y otros objetos con método aclose() pueden mantener conexiones, cursores, locks o bloques finally pendientes. Si un loop sale antes con break, return, cancelación o excepción, depender de la recolección puede retrasar la limpieza o ejecutarla fuera del contexto asíncrono correcto. contextlib.aclosing() convierte ese objeto en un async context manager que espera objeto.aclose() al salir.

Esta guía explica cierre determinista de generadores asíncronos, salidas anticipadas, context variables, excepciones, cancelación, diferencias con closing, integración con streams HTTP y cursores, ownership, AsyncExitStack y cuándo preferir el context manager nativo del recurso.

Primer bloque aclosing

from contextlib import aclosing

async with aclosing(generador_asincrono()) as valores:
    async for valor in valores:
        print(valor)

Al terminar el bloque, aclosing espera valores.aclose(). Esto sucede tanto al completar normalmente como durante el desempilado por una excepción.

Salida anticipada con break

async with aclosing(stream()) as items:
    async for item in items:
        if item.listo:
            break

Sin cierre explícito, el generador puede quedar suspendido. Con aclosing, sus bloques finally asíncronos se ejecutan antes de abandonar el contexto.

Generador con limpieza asíncrona

async def stream():
    recurso = await abrir_recurso()
    try:
        while True:
            item = await recurso.leer()
            if item is None:
                return
            yield item
    finally:
        await recurso.cerrar()

Llamar a aclose() solicita la terminación del generador y permite esperar su finally.

Por qué importa el contexto

La limpieza puede depender de context variables, loop actual, identidad de task, tracing o estado de excepción. Cerrar dentro del mismo async with mantiene esas dependencias predecibles.

Implementación conceptual

from contextlib import asynccontextmanager

@asynccontextmanager
async def aclosing_manual(objeto):
    try:
        yield objeto
    finally:
        await objeto.aclose()

El helper estándar expresa este patrón de forma consistente y elimina boilerplate repetido.

Diferencia con closing

from contextlib import closing

with closing(recurso) as valor:
    usar(valor)

closing() llama un close() síncrono. aclosing() espera un aclose() asíncrono. Usar la versión síncrona con una coroutine deja la limpieza sin await.

El objeto debe ofrecer aclose

aclosing no requiere herencia formal. Si el objeto no posee un aclose() apropiado, la salida del contexto falla. Una API pública puede describir la expectativa con Protocol:

from typing import Protocol

class CerrableAsync(Protocol):
    async def aclose(self) -> None: ...

Preferir el context manager nativo

Si el recurso implementa __aenter__ y __aexit__, úsalo directamente:

async with cliente.stream() as respuesta:
    ...

El contexto nativo puede realizar adquisición y liberación adicionales. aclosing es apropiado cuando aclose() representa realmente todo el contrato de cierre.

Clientes HTTP

Las bibliotecas HTTP difieren. Algunas respuestas exponen aclose; otras ofrecen un context manager que devuelve conexiones al pool, drena cuerpos y actualiza métricas. Sigue el contrato de la biblioteca y no envuelvas objetos automáticamente.

Cursores asíncronos

async with aclosing(cursor) as filas:
    async for fila in filas:
        if coincide(fila):
            return fila

Aunque exista un return anticipado, el cursor se cierra antes de devolver el resultado, reduciendo cursores, conexiones y locks filtrados.

Excepciones durante el consumo

async with aclosing(stream()) as items:
    async for item in items:
        procesar(item)  # puede lanzar

El context manager espera aclose durante el desempilado. Si la limpieza también falla, se aplican las reglas normales de exception chaining. Conserva la causa original y registra ambos fallos.

Cancelación

Una task puede cancelarse durante la iteración o durante aclose. La limpieza debe ser breve, idempotente y consciente de cancelación. Recursos críticos pueden requerir una región protegida estrecha, pero nunca deben bloquear la cancelación indefinidamente.

Timeout de limpieza

Si aclose puede quedar esperando red, crea un context manager personalizado:

import asyncio
from contextlib import asynccontextmanager

@asynccontextmanager
async def aclosing_con_timeout(objeto, segundos):
    try:
        yield objeto
    finally:
        async with asyncio.timeout(segundos):
            await objeto.aclose()

Define qué ocurre si el timeout vence y registra el estado del recurso.

Idempotencia

Idealmente, aclose() tolera múltiples llamadas o indica claramente que el objeto ya está cerrado. aclosing llama una vez por entrada de contexto, pero otro propietario puede intentar cerrar si el ownership es ambiguo.

No reutilizar después del cierre

Un generador asíncrono cerrado no vuelve a producir valores. Trata el bloque como todo su ciclo de vida y crea una instancia nueva para otro recorrido.

Ownership

Quien crea un recurso normalmente es responsable de cerrarlo. No envuelvas en aclosing un objeto prestado que otro componente continuará usando. Las funciones públicas deben declarar si consumen y cierran el stream recibido.

Factories y propiedad

async def consumir(factory):
    recurso = factory()
    async with aclosing(recurso) as items:
        async for item in items:
            ...

Recibir una factory deja claro que la función crea un recurso nuevo y asume su ciclo de vida.

Combinar con AsyncExitStack

from contextlib import AsyncExitStack, aclosing

async with AsyncExitStack() as stack:
    stream_a = await stack.enter_async_context(aclosing(crear_a()))
    stream_b = await stack.enter_async_context(aclosing(crear_b()))
    ...

AsyncExitStack gestiona una cantidad dinámica de recursos y los cierra en orden inverso.

Encapsular adquisición de dominio

from contextlib import asynccontextmanager, aclosing

@asynccontextmanager
async def filas_del_servicio():
    async with aclosing(crear_stream()) as stream:
        yield stream

El consumidor recibe un context manager de dominio y no necesita conocer aclose.

Context variables

Una propiedad importante es que la finalización ocurre en el mismo contexto de la iteración. El finally del generador puede leer contextvars para tracing, tenant, locale o credenciales temporales.

Varios streams

Asigna a cada stream propio su bloque aclosing o regístralo en AsyncExitStack. No supongas que salir de la función cerrará rápidamente todos los generadores suspendidos.

Generadores parcialmente consumidos

El caso principal es el consumo parcial. Si la iteración llega naturalmente al final, el generador ya termina, pero aclosing aplica una política determinista uniforme a todas las rutas.

Objetos que no son generadores

Cualquier objeto con aclose() asíncrono puede envolverse, incluidos canales, sesiones y wrappers. Confirma que ese método constituye todo el contrato de liberación.

Pruebas

class StreamFalso:
    def __init__(self):
        self.cerrado = False

    def __aiter__(self):
        return self

    async def __anext__(self):
        raise StopAsyncIteration

    async def aclose(self):
        self.cerrado = True

Prueba agotamiento normal, break, return, excepciones del consumidor, excepciones de limpieza y cancelación. Verifica que el cierre ocurra antes de completar la función externa.

Observabilidad

Mide streams abiertos, duración de cierre, timeouts y resultados de cancelación. No registres cuerpos de respuestas, valores de queries, tokens ni representaciones completas de recursos.

Errores comunes

  • Usar closing con aclose: la coroutine no se espera.
  • Envolver un objeto con contexto nativo más rico: pueden omitirse pasos de teardown.
  • Cerrar un recurso prestado: define ownership.
  • Depender de la recolección del generador: el finally puede ejecutarse tarde.
  • Ignorar cancelación durante la limpieza: el recurso puede quedar a medias.
  • Reutilizar el stream cerrado: su ciclo de vida terminó.

Ejemplo completo: búsqueda anticipada

from contextlib import aclosing

async def encontrar_primero(factory, predicado):
    async with aclosing(factory()) as stream:
        async for item in stream:
            if predicado(item):
                return item
    return None

La función devuelve el primer resultado, pero el generador se cierra antes de que la coroutine entregue el valor al llamador.

Conclusión

contextlib.aclosing() proporciona limpieza determinista para objetos con aclose(), especialmente generadores asíncronos consumidos parcialmente. Mantiene la finalización en el mismo contexto y hace más seguros break, return, cancelación y excepciones.

La documentación oficial de contextlib.aclosing define el helper. Prefiere el context manager nativo cuando exista y usa aclosing cuando aclose sea la interfaz completa de liberación.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Contenido del artículo

    Artículos relacionados

    A person typing on a laptop with a Python programming book visible, capturing technology and learning.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    nullcontext en Python: contextos opcionales

    Usa nullcontext en Python para unificar archivos, locks, transacciones, sesiones y recursos prestados sin duplicar ramas.

    Ler mais

    Tempo de leitura: 4 minutos
    30/08/2026
    High-angle view of woman coding on a laptop, with a Python book nearby. Ideal for programming and tech content.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    weakref.finalize: limpieza sin retener objetos

    Aprende weakref.finalize en Python para limpiar recursos sin retener objetos, usando alive, detach, shutdown y cierre explícito seguro.

    Ler mais

    Tempo de leitura: 5 minutos
    30/08/2026
    A developer typing code on a laptop with a Python book beside in an office.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    SimpleNamespace: objetos ligeros con atributos

    Aprende SimpleNamespace en Python para crear objetos ligeros por atributos, convertir diccionarios, copiar y elegir modelos tipados.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026
    Close-up of a detailed South America map showcasing geography and cartography.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    ChainMap en Python: mapas por capas

    Aprende ChainMap en Python para combinar configuración y scopes por capas, controlar precedencia, escrituras y snapshots seguros.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026
    A developer typing code on a laptop with a Python book beside in an office.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    itertools.pairwise: analiza pares consecutivos

    Aprende itertools.pairwise en Python para analizar pares consecutivos, calcular deltas, detectar transiciones, huecos y errores de orden.

    Ler mais

    Tempo de leitura: 4 minutos
    29/08/2026
    A close-up shot showcasing the intricate scales of a snake, highlighting texture and color.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    itertools.batched: procesa iterables por lotes

    Aprende itertools.batched en Python para procesar iterables por lotes, controlar memoria, usar strict y crear pipelines resilientes.

    Ler mais

    Tempo de leitura: 6 minutos
    29/08/2026