contextlib - Utilità per gestore di contesto

Scopo Utilità per creare e lavorare con gestori di contesto
Versione Python 2.5 e successivo

Il modulo contextlib contiene utilità per lavorare con gestori di contesto e con l'istruzione with.

I gestori di contesto sono legati all'istruzione with. Visto che with è ufficialmente parte di Python 2.6, prima di utilizzarlo in Python 2.5 occorre importarlo tramite __future__

API del Gestore di Contesto

Un gestore di contesto (context manager) è responsabile per una risorsa all'interno di un blocco di codice, che possibilmente dallo stesso viene creata quando il flusso del codice entra nel blocco, quindi viene ripulita quando il flusso esce dal blocco. Ad esempio, gli oggetti file supportano l'API del gestore di contesto per facilitare l'assicurarsi che essi vengano chiusi dopo che tutte le operazioni di lettura e scrittura sono terminate.

with open('/tmp/pymotw.txt', 'wt') as f:
    f.write('il contenuto va qui')
# il file viene chiuso automaticamente

Un gestore di contesto viene abilitato dall'istruzione with, e l'API coinvolge due metodi: __enter__() viene eseguito quando il flusso di esecuzione entra nel blocco di codice all'interno di with. Esso ritorna un oggetto da utilizzare all'interno del contesto. Quando il flusso di esecuzione abbandona il blocco with, viene chiamato il metodo __exit__() del gestore di contesto per ripulire qualsivoglia risorsa sia stata utilizzata.

class Context(object):

    def __init__(self):
        print '__init__()'

    def __enter__(self):
        print '__enter__()'
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print '__exit__()'

with Context():
    print 'Lavoro eseguito nel contesto'

La combinazione di un gestore di contesto e dell'istruzione with costituisce un modo più compatto di scrivere un blocco try:finally, visto che il metodo __exit__() del gestore di contesto viene sempre chiamato, anche se viene sollevata una eccezione.

$ python contextlib_api.py

__init__()
__enter__()
Lavoro eseguito nel contesto
__exit__()

Il metodo __enter__() può restituire un qualsiasi oggetto che venga associato ad un nome specificato nella clausola as dell'istruzione with. In questo esempio, Context ritorna un oggetto che utilizza il contesto aperto.

class DentroIlContesto(object):

    def __init__(self, context):
        print 'DentroIlContesto.__init__(%s)' % context

    def do_something(self):
        print 'DentroIlContesto.do_something()'

    def __del__(self):
        print 'DentroIlContesto.__del__'


class Context(object):

    def __init__(self):
        print 'Context.__init__()'

    def __enter__(self):
        print 'Context.__enter__()'
        return DentroIlContesto(self)

    def __exit__(self, exc_type, exc_val, exc_tb):
        print 'Context.__exit__()'

with Context() as c:
    c.do_something()

Può confondere un tantino, ma il valore associato alla variabile c è l'oggetto restituito da __enter__() e non dall'istanza di Context creata nell'istruzione with.

$ python contextlib_api_other_object.py

Context.__init__()
Context.__enter__()
DentroIlContesto.__init__(<__main__.Context object at 0x7f649bca8750>)
DentroIlContesto.do_something()
Context.__exit__()
DentroIlContesto.__del__

Il metodo __exit__() riceve argomenti che contengono dettagli di qualsiasi eccezione venga sollevata all'interno del blocco with.

class Context(object):

    def __init__(self, handle_error):
        print '__init__(%s)' % handle_error
        self.handle_error = handle_error

    def __enter__(self):
        print '__enter__()'
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print '__exit__(%s, %s, %s)' % (exc_type, exc_val, exc_tb)
        return self.handle_error

with Context(True):
    raise RuntimeError('messaggio di errore gestito')

print

with Context(False):
    raise RuntimeError('messaggio di errore propagato')

Se il gestore di contesto può gestire l'eccezione, __exit__() dovrebbe restituire un valore True per indicare che l'eccezione non deve propagarsi. Restituire False fa sì che l'eccezione sia sollevata dopo che __exit__() ritorna.

$ python contextlib_api_error.py

__init__(True)
__enter__()
__exit__(, messaggio di errore gestito, )

__init__(False)
__enter__()
__exit__(, messaggio di errore propagato, )
Traceback (most recent call last):
  File "contextlib_api_error.py", line 24, in 
    raise RuntimeError('messaggio di errore propagato')
RuntimeError: messaggio di errore propagato

Dal Generatore al Gestore di Contesto

Creare gestori di contesto nel modo tradizionale, scrivendo una classe che abbia i metodi __enter__() ed __exit__() non è difficile. Tuttavia qualche volta può essere inutilmente dispendioso rispetto alla necessità di gestire un pezzo triviale di contesto. In questo tipo di situazioni si potrebbe utilizzare il decoratore contextmanager() per convertire una funzione generatore in un gestore di contesto.

import contextlib

@contextlib.contextmanager
def make_context():
    print '  in entrata'
    try:
        yield {}
    except RuntimeError, err:
        print '  ERRORE:', err
    finally:
        print '  in uscita'

print 'Normale:'
with make_context() as value:
    print '  dentro l\'istruzione with:', value

print
print 'Errore gestito:'
with make_context() as value:
    raise RuntimeError('si mostra un esempio di gestione di un errore')

print
print 'Errore non gestito:'
with make_context() as value:
    raise ValueError('questa eccezione non viene gestita')

Il generatore dovrebbe inizializzare il contesto, trattenerlo esattamente una volta, quindi ripulire il contesto. Il valore trattenuto, se esiste, viene legato alla variabile nella clausola as dell'istruzione with. Le eccezioni all'interno del blocco with sono nuovamente sollevate all'interno del generatore, in modo che ivi possano essere trattate.

$ python contextlib_contextmanager.py

Normale:
  in entrata
  dentro l'istruzione with: {}
  in uscita

Errore gestito:
  in entrata
  ERRORE: si mostra un esempio di gestione di un errore
  in uscita

Errore non gestito:
  in entrata
  in uscita
Traceback (most recent call last):
  File "contextlib_contextmanager.py", line 28, in 
    raise ValueError('questa eccezione non viene gestite')
ValueError: questa eccezione non viene gestita

Annidare Contesti

A volte risulta necessario gestire simultaneamente contesti multipli (tipo quando si copiano dati da handle di file in input ed output, ad esempio). E' possibile annidare istruzioni with una all'interno dell'altra. Se il contesto più esterno non necessita del suo proprio blocco separato, tuttavia, si deve aggiungere un livello di indentazione senza avere un reale beneficio. Utilizzando nested() i contesti vengono annidati usando una singola istruzione with

import contextlib

@contextlib.contextmanager
def make_context(name):
    print 'in entrata:', name
    yield name
    print 'in uscita :', name

with contextlib.nested(make_context('A'), make_context('B'), make_context('C')) as (A, B, C):
    print 'all\'interno dell\'istruzione with:', A, B, C

Si noti che si esce dai contesti nell'ordine inverso rispetto a quello di entrata.

$ python contextlib_nested_with.py

in entrata: A
in entrata: B
in entrata: C
all'interno dell'istruzione with: A B C
in uscita : C
in uscita : B
in uscita : A

In Python 2.7 e superiore, nested() è deprecato visto che l'istruzione with supporta l'annidamento direttamente.

import contextlib

@contextlib.contextmanager
def make_context(name):
    print 'in entrata:', name
    yield name
    print 'in uscita :', name

with contextlib.nested(make_context('A'), make_context('B'), make_context('C')) as (A, B, C):
    print 'all\'interno dell\'istruzione with:', A, B, C

Ciascun gestore di contesto e clausola opzionale as è separato da un virgola ,. L'effetto è simile a quello con l'utilizzo di nested(), ma evita alcuni dei casi limite riguardo alla gestione di errori che nested() non può implementare correttamente.

$ python contextlib_nested_with.py
entering: A
entering: B
entering: C
all'interno dell'istruzione with: A B C
exiting : C
exiting : B
exiting : A

Chiudere Handle Aperti

La classe file supporta direttamente l'API del gestore di contesto, ma taluni altri oggetti che rappresentano handle aperti non lo fanno. L'esempio fornito nella documentazione della libreria standard per contextlib è l'oggetto restituito da urllib.urlopen(). Ci sono altre classi legacy che utilizzano un metodo close() ma non supportano l'API del gestore di contesto. Per assicurarsi che l'handle venga chiuso, si usi closing() per creare un gestore di contesto per la situazione.

import contextlib

class Door(object):
    def __init__(self):
        print '  __init__()'
    def close(self):
        print '  close()'

print 'Esempio Normale:'
with contextlib.closing(Door()) as door:
    print '  dentro l\'istruzione with'

print
print 'Esempio di gestione errore:'
try:
    with contextlib.closing(Door()) as door:
        print '  sollevata da dentro l\'istruzione with'
        raise RuntimeError('messaggio di errore')
except Exception, err:
    print '  Si è verificato un errore:', err

L'handle viene chiuso a prescindere che si verifichi un errore nel blocco with.

$ python contextlib_clong.py

Esempio Normale:
  __init__()
  dentro l'istruzione with
  close()

Esempio di gestione errore:
  __init__()
  sollevata da dentro l'istruzione with
  close()
  Si è verificato un errore: messaggio di errore

Vedere anche:

contextlib
La documentazione della libreria standard per questo modulo
PEP 343
L'istruzione with
Tipi di Gestori di Contesto
Descrizione dell'API del gestore di contesto dalla documentazione della libreria standard (in inglese - n.d.t.)
Gestori di Contesto nell'istruzione with
Descrizione dell'API del gestore di contesto dalla guida di riferimento di Python (in inglese - n.d.t.)