atexit - Callback alla Chiusura di un Programma

Scopo: Registra una o più funzioni (callback) che saranno chiamate quando un programma è in chiusura

Il modulo atexit fornisce una interfaccia per registrare funzioni che saranno chiamate quando un programma si sta normalmente chiudendo.

Registrare Callback in Uscita

Il seguente è un esempio per registrare una funzione chiamando esplicitamente register():

# atexit_simple.py

import atexit


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


print('In registrazione')
atexit.register(all_done)
print('Registrato')

Visto che il programma non fa null'altro, all_done() viene chiamata immediatamente.

$ python3 atexit_simple.py

In registrazione
Registrato
all_done()

E' anche possibile registrare più di una funzione, e fornire argomenti alle funzioni registrate. Questo può essere utile per disconnettersi in modo pulito da database, rimuovere file temporanei ecc.. Invece che mantenere una lista di risorse che devono essere liberate, si può registrare una funzione di pulizia separata per ogni risorsa.

# atexit_multiple.py

import atexit


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


atexit.register(my_cleanup, 'primo')
atexit.register(my_cleanup, 'secondo')
atexit.register(my_cleanup, 'terzo')

L'ordine di chiamata delle funzioni in uscita è inverso rispetto a quello di registrazione. Questo metodo consente di pulire i moduli nell'ordine inverso rispetto a quello di importazione (e di conseguenza di registrazione delle loro funzioni atexit), il che dovrebbe ridurre i conflitti di dipendenza.

$ python3 atexit_multiple.py

my_cleanup(terzo)
my_cleanup(secondo)
my_cleanup(primo)

Sintassi per Decoratore

Le funzioni che non richiedono argomenti possono essere registrate utilizzando register() come un decoratore. Questa sintassi alternativa è conveniente per funzioni di pulizia che operano su dati globali a livello di modulo.

# atexit_decorator.py

import atexit


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


print('partenza programma principale')

Visto che la funzione è registrata così com'è definita, è anche importante assicurarsi che essa funzioni regolarmente anche se nessun altra attività viene eseguita dal modulo. Se le risorse che si suppone debbano essere pulite non sono mai inizializzate, la chiamata della funzione di uscita non dovrebbe produrre un errore.

$ python3 atexit_decorator.py

partenza programma principale
all_done()

Cancellare Callback

Per cancellare un callback di uscita, lo si rimuova dal registro usando unregister().

# atexit_unregister.py

import atexit


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


atexit.register(my_cleanup, 'primo')
atexit.register(my_cleanup, 'secondo')
atexit.register(my_cleanup, 'terzo')

atexit.unregister(my_cleanup)

Tutte le chiamate allo stesso callback sono cancellate, a prescindere da quante volte è stato registrato.

$ python3 atexit_unregister.py

La rimozione di un callback che non era stato precedentemente registrato non viene considerato un errore.

# atexit_unregister_not_registered.py

import atexit


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


if False:
    atexit.register(my_cleanup, 'mai registrato')

atexit.unregister(my_cleanup)

Visto che ignora in modo silente callback sconosciuti, unregister() può essere usato anche quando la sequenza delle registrazioni potrebbe non essere nota.

$ python3 atexit_unregister_not_registered.py

Quando Ci Sono Callback Di atexit Non Chiamati?

I callback registrati con atexit non sono chiamati se si verifica almeno una delle seguenti condizioni:

  • Il programma termina a causa di un segnale
  • os._exit() viene invocato direttamente
  • Viene rilevato un errore fatale nell'interprete

Un esempio dall'articolo su subprocess può essere aggiornato per mostrare cosa succede quando un programma viene terminato da un segnale. Sono coinvolti due file, il programma genitore e il figlio. Il genitore fa partire il figlio, si mette in pausa, quindi lo termina.

# atexit_signal_parent.py

import os
import signal
import subprocess
import time

proc = subprocess.Popen(['python3', './atexit_signal_child.py'])
print('GENITORE: In pausa prima di inviare il segnale...')
time.sleep(1)
print('GENITORE: Invio segnale al figlio')
os.kill(proc.pid, signal.SIGTERM)

Il figlio imposta un callback atexit di uscita, poi si mette in pausa fino a che arriva il segnale.

# atexit_signal_child.py

import atexit
import time
import sys


def not_called():
    print('FIGLIO: il gestore di atexit non dovrebbe essere stato chiamato')


print('FIGLIO: Registrazione del gestore atexit')
sys.stdout.flush()
atexit.register(not_called)

print('FIGLIO: In pausa in attesa del segnale')
sys.stdout.flush()
time.sleep(5)

Quando eseguito, questo è il risultato.

$ python3 atexit_signal_parent.py

GENITORE: In pausa prima di inviare il segnale...
FIGLIO: Registrazione del gestore atexit
FIGLIO: In pausa in attesa del segnale
GENITORE: Invio segnale al figlio

Il figlio non stampa il messaggio inserito in not_called().

Se il programma usa os._exit(), può evitare la chiamata dei callback atexit.

# atexit_os_exit.py

import atexit
import os


def not_called():
    print('Questa non dovrebbe essere chiamata')


print('In registrazione')
atexit.register(not_called)
print('Registrato')

print('In uscita...')
os._exit(0)

Visto che l'esempio salta il normale percorso di uscita, il callback non viene eseguito. Anche la stampa del risultato non viene scaricata, quindi l'esempio è eseguito con l'opzione -u per abiltare l'I/O senza buffer.

$ python3 -u atexit_os_exit.py

In registrazione
Registrato
In uscita...

Per assicurarsi che i callback vengano eseguiti, si consenta al programma di terminare esaurendo le istruzioni da eseguire oppure chiamando sys.exit().

# atexit_sys_exit.py
import atexit
import sys


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


print('In registrazione')
atexit.register(all_done)
print('Registrato')

print('In uscita...')
sys.exit()

Questo esempio chiama sys.exit(), quindi i callback registrati vengono chiamati.

$ python3 atexit_sys_exit.py

In registrazione
Registrato
In uscita...
all_done()

Gestire Eccezioni

I traceback per le eccezioni sollevate in atexit sono stampati alla console e l'ultima eccezione sollevata viene risollevata in modo da essere l'ultimo messaggio di errore del programma.

# atexit_exception.py
import atexit


def exit_with_exception(message):
    raise RuntimeError(message)


atexit.register(exit_with_exception, 'Registrato per primo')
atexit.register(exit_with_exception, 'Registrato per secondo')

L'ordine di registrazione controlla l'ordine di esecuzione. Se un errore in un callback ne introduce un errore in un altro (registrato in precedenza ma chiamato successivamente), il messaggio di errore finale potrebbe non essere quello più utile da mostrare all'utente.

$ python3 atexit_exception.py

Error in atexit._run_exitfuncs:
Traceback (most recent call last):
  File "atexit_exception.py", line 6, in exit_with_exception
    raise RuntimeError(message)
RuntimeError: Registrato per secondo
Error in atexit._run_exitfuncs:
Traceback (most recent call last):
  File "atexit_exception.py", line 6, in exit_with_exception
    raise RuntimeError(message)
RuntimeError: Registrato per primo

Vedere anche:

atexit
La documentazione della libreria standard per questo modulo
Gestione delle eccezioni (di prossima traduzione)
Gestione globale delle eccezioni non catturate
Note di portabilità per atexit