atexit — Devoluciones de llamada de salida para programas

Propósito:Registra funciones a las que se llamará cuando un programa finalice.

El módulo atexit proporciona una interfaz para registrar funciones a las que se llamará cuando un programa se finalice normalmente.

Registro de devoluciones de llamada de salida

Este es un ejemplo de registro de una función explícitamente llamando a register().

atexit_simple.py
import atexit


def all_done():
    print('all_done()')


print('Registering')
atexit.register(all_done)
print('Registered')

Debido a que el programa no hace nada más, all_done() se llama de inmediato.

$ python3 atexit_simple.py

Registering
Registered
all_done()

También es posible registrar más de una función y pasar argumentos a las funciones registradas. Eso puede ser útil para desconectarse limpiamente de las bases de datos, eliminar archivos temporales, etc. En lugar de mantener una lista de recursos que deben liberarse, se puede registrar una función de limpieza separada para cada recurso.

atexit_multiple.py
import atexit


def my_cleanup(name):
    print('my_cleanup({})'.format(name))


atexit.register(my_cleanup, 'first')
atexit.register(my_cleanup, 'second')
atexit.register(my_cleanup, 'third')

Las funciones de salida se invocan al revés del orden en que se registran. Este método permite limpiar los módulos en el orden inverso desde el que se importan (y, por lo tanto, registran sus funciones atexit), lo que debería reducir los conflictos de dependencia.

$ python3 atexit_multiple.py

my_cleanup(third)
my_cleanup(second)
my_cleanup(first)

Sintaxis del decorador

Las funciones que no requieren argumentos pueden registrarse utilizando register() como decorador. Esta sintaxis alternativa es conveniente para las funciones de limpieza que operan en datos globales a nivel de módulo.

atexit_decorator.py
import atexit


@atexit.register
def all_done():
    print('all_done()')


print('starting main program')

Debido a que la función se registra tal como está definida, también es importante asegurarse de que funciona correctamente incluso si el módulo no realiza ningún otro trabajo. Si los recursos que se supone que debe limpiar nunca se inicializaron, llamar a la devolución de llamada de salida no debería producir un error.

$ python3 atexit_decorator.py

starting main program
all_done()

Cancelar devoluciones de llamada

Para cancelar una devolución de llamada de salida, elimínala del registro usando unregister().

atexit_unregister.py
import atexit


def my_cleanup(name):
    print('my_cleanup({})'.format(name))


atexit.register(my_cleanup, 'first')
atexit.register(my_cleanup, 'second')
atexit.register(my_cleanup, 'third')

atexit.unregister(my_cleanup)

Todas las llamadas a la misma devolución de llamada se cancelan, independientemente de cuántas veces se haya registrado.

$ python3 atexit_unregister.py

Eliminar una devolución de llamada que no se registró previamente no se considera un error.

atexit_unregister_not_registered.py
import atexit


def my_cleanup(name):
    print('my_cleanup({})'.format(name))


if False:
    atexit.register(my_cleanup, 'never registered')

atexit.unregister(my_cleanup)

Debido a que ignora silenciosamente las devoluciones de llamada desconocidas, unregister() puede usarse incluso cuando la secuencia de registros puede no ser conocida.

$ python3 atexit_unregister_not_registered.py

¿Cuándo no se llaman las devoluciones de llamada de atexit?

Las devoluciones de llamada registradas con atexit no se invocan si se cumple alguna de estas condiciones:

  • El programa termina debido a una señal.
  • os._exit() se invoca directamente.
  • Se detectó un error fatal en el intérprete.

Un ejemplo de la sección subprocess se puede actualizar para mostrar lo que sucede cuando un programa es terminado por una señal. Hay dos archivos involucrados, los programas padre e hijo. El padre inicia al hijo, hace una pausa y luego lo termina.

atexit_signal_parent.py
import os
import signal
import subprocess
import time

proc = subprocess.Popen('./atexit_signal_child.py')
print('PARENT: Pausing before sending signal...')
time.sleep(1)
print('PARENT: Signaling child')
os.kill(proc.pid, signal.SIGTERM)

El hijo configura una devolución de llamada atexit y luego duerme hasta que llega la señal.

atexit_signal_child.py
import atexit
import time
import sys


def not_called():
    print('CHILD: atexit handler should not have been called')


print('CHILD: Registering atexit handler')
sys.stdout.flush()
atexit.register(not_called)

print('CHILD: Pausing to wait for signal')
sys.stdout.flush()
time.sleep(5)

Cuando se ejecuta, ésta es la salida.

$ python3 atexit_signal_parent.py

CHILD: Registering atexit handler
CHILD: Pausing to wait for signal
PARENT: Pausing before sending signal...
PARENT: Signaling child

El niño no imprime el mensaje embebido en not_called.

Si un programa usa os._exit(), puede evitar que se invoquen las devoluciones de llamada atexit.

atexit_os_exit.py
import atexit
import os


def not_called():
    print('This should not be called')


print('Registering')
atexit.register(not_called)
print('Registered')

print('Exiting...')
os._exit(0)

Como este ejemplo omite la ruta de salida normal, la devolución de llamada no se ejecuta. La salida de impresión tampoco se vacía, por lo que el ejemplo se ejecuta con la opción -u para habilitar la E/S sin búfer.

$ python3 -u atexit_os_exit.py

Registering
Registered
Exiting...

Para garantizar que se ejecuten las devoluciones de llamada, permite que el programa finalice agotando las instrucciones para ejecutar o llamando a sys.exit().

atexit_sys_exit.py
import atexit
import sys


def all_done():
    print('all_done()')


print('Registering')
atexit.register(all_done)
print('Registered')

print('Exiting...')
sys.exit()

Este ejemplo llama a sys.exit(), por lo que se invocan las devoluciones de llamada registradas.

$ python3 atexit_sys_exit.py

Registering
Registered
Exiting...
all_done()

Manejo de excepciones

Los rastreos para las excepciones generadas en las devoluciones de llamada atexit se imprimen en la consola y la última excepción planteada se vuelve a generar para ser el mensaje de error final del programa.

atexit_exception.py
import atexit


def exit_with_exception(message):
    raise RuntimeError(message)


atexit.register(exit_with_exception, 'Registered first')
atexit.register(exit_with_exception, 'Registered second')

La orden de registro controla la orden de ejecución. Si un error en una devolución de llamada introduce un error en otra (registrado anteriormente, pero llamado más tarde), el mensaje de error final podría no ser el mensaje de error más útil para mostrar al usuario.

$ python3 atexit_exception.py

Error in atexit._run_exitfuncs:
Traceback (most recent call last):
  File "atexit_exception.py", line 11, in exit_with_exception
    raise RuntimeError(message)
RuntimeError: Registered second
Error in atexit._run_exitfuncs:
Traceback (most recent call last):
  File "atexit_exception.py", line 11, in exit_with_exception
    raise RuntimeError(message)
RuntimeError: Registered first

Por lo general, es mejor manejar y registrar silenciosamente todas las excepciones en las funciones de limpieza, ya que es complicado tener errores de volcado de programa al salir.