Recursos externos como archivos temporales, handles nativos, sockets auxiliares y registros de caches deben liberarse. El camino principal debe usar context managers y métodos explícitos, pero algunas bibliotecas necesitan una red de seguridad cuando un objeto es recolectado sin cerrarse. weakref.finalize registra un callback que permanece vivo sin mantener vivo el objeto observado y se ejecuta cuando ese objeto deja de ser alcanzable.
Esta guía explica cómo crear finalizers, evitar referencias fuertes accidentales, ejecutar limpieza anticipada, usar detach(), inspeccionar alive, comprender el cierre del intérprete, probar el comportamiento y decidir por qué la recolección no debe ser el mecanismo principal.
Primer finalizer
import weakref
class Recurso:
pass
def limpiar(nombre):
print("limpiando", nombre)
recurso = Recurso()
finalizer = weakref.finalize(recurso, limpiar, "temporal")
Mientras recurso sea alcanzable, el callback no se ejecuta. Cuando deja de serlo, el finalizer puede llamar a limpiar("temporal").
No mantiene vivo el objeto
La asociación usa una referencia débil. Sin embargo, los argumentos del callback se conservan fuertemente. Si uno apunta al objeto observado, puede impedir la recolección.
# Evita esto:
weakref.finalize(recurso, recurso.cerrar)
Un método vinculado mantiene viva su instancia. Prefiere una función independiente con solo los datos externos necesarios:
weakref.finalize(recurso, cerrar_handle, recurso.handle)
Limpieza idempotente
La limpieza debe tolerar peticiones repetidas o coordinar estado para liberar el recurso una sola vez. Un finalizer concreto llama al callback como máximo una vez, pero la clase también puede ofrecer close().
Ejecución anticipada
finalizer()
Llamar al objeto ejecuta inmediatamente el callback si sigue activo y devuelve su resultado. Las llamadas posteriores no repiten la acción.
Propiedad alive
if finalizer.alive:
finalizer()
alive indica si el callback continúa registrado y no se ha ejecutado ni separado.
detach
detalles = finalizer.detach()
detach() desactiva el finalizer y, si estaba vivo, devuelve una tupla con objeto, función, argumentos y kwargs. Es útil para transferir responsabilidad de limpieza.
peek
detalles = finalizer.peek()
peek() inspecciona el registro sin desactivarlo. La tupla puede mantener temporalmente una referencia fuerte al objeto; no la conserves sin necesidad.
Close explícito con fallback
class ArtefactoTemporal:
def __init__(self, ruta):
self.ruta = ruta
self._finalizer = weakref.finalize(
self,
eliminar_archivo,
ruta,
)
def close(self):
self._finalizer()
El método explícito proporciona liberación determinista. El finalizer queda como respaldo cuando el consumidor olvida cerrar.
Context managers siguen siendo preferibles
with ArtefactoTemporal(ruta) as recurso:
usar(recurso)
Un context manager garantiza limpieza al terminar el bloque, incluso con excepciones. El momento de la recolección no está garantizado en todas las implementaciones.
Por qué no depender de __del__
__del__ complica herencia, ciclos, excepciones, objetos parcialmente inicializados y shutdown. weakref.finalize separa la función de limpieza y ofrece control por llamada, alive y detach().
El momento no está garantizado
No bases la corrección en cuándo se ejecuta el callback. Referencias pueden permanecer en caches, closures, tracebacks, tasks o threads. Distintas implementaciones de Python recolectan en momentos diferentes.
Basura cíclica
Los finalizers cooperan mejor con el recolector, pero el callback debe evitar resucitar objetos o depender del orden de finalización de un ciclo.
Orden entre recursos
Si varios objetos se vuelven inalcanzables juntos, no supongas una secuencia implícita. Modela propiedad explícitamente para que un manager cierre dependencias antes de sí mismo.
Cierre del intérprete
Finalizers vivos pueden ejecutarse al salir, normalmente en orden inverso de creación. La propiedad atexit controla la participación:
finalizer.atexit = False
Durante shutdown, módulos y globales pueden estar parcialmente desmontados. Pasa funciones independientes y valores concretos al callback.
Excepciones en callbacks
Las excepciones de finalización automática no se propagan normalmente al código que causó la recolección. Mantén callbacks pequeños, captura fallos esperados y registra con seguridad. No uses un finalizer como único lugar para persistir información crítica en red.
Threads
El callback puede ejecutarse en un contexto distinto a la thread creadora. Evita depender de estado thread-local. Si la liberación requiere una thread específica, prográmala explícitamente.
Recursos asyncio
Un finalizer es síncrono y no puede hacer await. No llames una coroutine de cierre directamente. Ofrece async with y aclose(); como máximo, el finalizer puede emitir una advertencia o señalizar un loop activo con mucho cuidado.
Archivos temporales
from pathlib import Path
import weakref
class Artefacto:
def __init__(self, ruta: Path):
self.ruta = ruta
self._cleanup = weakref.finalize(
self,
Path.unlink,
ruta,
missing_ok=True,
)
def eliminar(self):
self._cleanup()
La función y la ruta no referencian la instancia. missing_ok=True hace idempotente la eliminación si otro camino ya borró el archivo.
Handles nativos
Al integrar bibliotecas C, conserva en los argumentos solo el identificador y una función segura de liberación. Comprueba que la biblioteca nativa siga disponible durante el shutdown.
Objetos retenidos por caches
Si un cache mantiene una referencia fuerte, el finalizer no puede ejecutarse. Considera WeakValueDictionary o una política explícita de eviction.
Pruebas
Las pruebas deben ejercitar primero la limpieza explícita:
finalizer()
assert not finalizer.alive
finalizer() # no repite
Una prueba separada puede comprobar el fallback, pero evita depender totalmente del momento específico de gc.collect().
Evitar closures que capturan self
# Incorrecto: captura self
weakref.finalize(self, lambda: liberar(self.handle))
Copia el handle a un valor independiente y pásalo a una función de nivel superior o estática.
Transferir propiedad
Cuando el recurso pasa a otro objeto, separa el finalizer antiguo y registra uno nuevo en el nuevo propietario. Así evitas dos componentes liberando el mismo handle.
Observabilidad
El callback de fallback puede incrementar una métrica indicando que faltó cierre explícito. Evita logs ruidosos al salir y no incluyas objetos completos ni identificadores secretos.
Rendimiento
Cada finalizer tiene coste de registro y seguimiento. No crees uno para millones de objetos pequeños cuando un propietario mayor puede gestionar recursos en bloque.
Errores comunes
- Pasar un método vinculado del objeto: mantiene viva la instancia.
- Usarlo como camino principal: prefiere close y context managers.
- Intentar ejecutar una coroutine: el finalizer es síncrono.
- Depender del orden de callbacks: modela propiedad explícita.
- Buscar globales durante shutdown: los módulos pueden estar desmontados.
- Ignorar idempotencia: los caminos explícito y fallback deben coordinarse.
Ejemplo completo: recurso nativo
import weakref
class ConexionNativa:
def __init__(self, api):
handle = api.abrir()
self._api = api
self._handle = handle
self._finalizer = weakref.finalize(
self,
api.cerrar,
handle,
)
@property
def cerrada(self):
return not self._finalizer.alive
def close(self):
self._finalizer()
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
self.close()
El uso normal es determinista mediante with. Si el consumidor olvida cerrar, el finalizer todavía intenta liberar el handle sin retener la instancia.
Conclusión
weakref.finalize proporciona una red de seguridad controlable para limpieza asociada al ciclo de vida de un objeto. Evita varias debilidades de __del__, pero no convierte la recolección en gestión determinista.
La documentación oficial de weakref.finalize define la API. Usa context managers como camino principal, no captures el objeto observado en el callback y mantén la finalización pequeña, idempotente y segura durante shutdown.







