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.







