Differenziazione tra le possibili origini delle eccezioni sollevate da una with
dichiarazione composta
La differenziazione tra le eccezioni che si verificano in with
un'istruzione è complicata perché possono avere origine in luoghi diversi. Eccezioni possono essere sollevate da uno dei seguenti luoghi (o funzioni chiamate in esso):
ContextManager.__init__
ContextManager.__enter__
- il corpo del
with
ContextManager.__exit__
Per ulteriori dettagli, consultare la documentazione sui tipi di Gestore di contesto .
Se vogliamo distinguere tra questi diversi casi, basta avvolgere with
in a try .. except
non è sufficiente. Considera il seguente esempio (usando ValueError
come esempio ma ovviamente potrebbe essere sostituito con qualsiasi altro tipo di eccezione):
try:
with ContextManager():
BLOCK
except ValueError as err:
print(err)
Qui except
cattureranno le eccezioni che hanno origine in tutti e quattro i diversi luoghi e quindi non consentono di distinguerli. Se spostiamo l'istanza dell'oggetto gestore di contesto all'esterno di with
, possiamo distinguere tra __init__
e BLOCK / __enter__ / __exit__
:
try:
mgr = ContextManager()
except ValueError as err:
print('__init__ raised:', err)
else:
try:
with mgr:
try:
BLOCK
except TypeError: # catching another type (which we want to handle here)
pass
except ValueError as err:
# At this point we still cannot distinguish between exceptions raised from
# __enter__, BLOCK, __exit__ (also BLOCK since we didn't catch ValueError in the body)
pass
In effetti, questo ha aiutato la __init__
parte, ma possiamo aggiungere una variabile sentinella aggiuntiva per verificare se il corpo with
dell'inizio ha iniziato l'esecuzione (ovvero differenziando tra __enter__
e gli altri):
try:
mgr = ContextManager() # __init__ could raise
except ValueError as err:
print('__init__ raised:', err)
else:
try:
entered_body = False
with mgr:
entered_body = True # __enter__ did not raise at this point
try:
BLOCK
except TypeError: # catching another type (which we want to handle here)
pass
except ValueError as err:
if not entered_body:
print('__enter__ raised:', err)
else:
# At this point we know the exception came either from BLOCK or from __exit__
pass
La parte difficile è quella di distinguere tra le eccezioni originate da BLOCK
e __exit__
perché with
viene passata un'eccezione che sfugge al corpo della volontà __exit__
che può decidere come gestirla (vedi i documenti ). Se tuttavia si __exit__
solleva, l'eccezione originale verrà sostituita da quella nuova. Per far fronte a questi casi, possiamo aggiungere una except
clausola generale nel corpo dell'archivio with
per archiviare qualsiasi potenziale eccezione che altrimenti sarebbe sfuggita inosservata e confrontarla con quella rilevata in except
seguito più esterna - se sono uguali ciò significa che l'origine era BLOCK
o altrimenti lo era __exit__
(nel caso __exit__
sopprimesse l'eccezione restituendo un valore vero il più esternoexcept
semplicemente non verrà eseguito).
try:
mgr = ContextManager() # __init__ could raise
except ValueError as err:
print('__init__ raised:', err)
else:
entered_body = exc_escaped_from_body = False
try:
with mgr:
entered_body = True # __enter__ did not raise at this point
try:
BLOCK
except TypeError: # catching another type (which we want to handle here)
pass
except Exception as err: # this exception would normally escape without notice
# we store this exception to check in the outer `except` clause
# whether it is the same (otherwise it comes from __exit__)
exc_escaped_from_body = err
raise # re-raise since we didn't intend to handle it, just needed to store it
except ValueError as err:
if not entered_body:
print('__enter__ raised:', err)
elif err is exc_escaped_from_body:
print('BLOCK raised:', err)
else:
print('__exit__ raised:', err)
Approccio alternativo usando la forma equivalente menzionata nel PEP 343
PEP 343 - L'istruzione "with" specifica una versione equivalente "non con" with
dell'istruzione. Qui possiamo facilmente avvolgere le varie parti con try ... except
e quindi differenziare tra le diverse potenziali fonti di errore:
import sys
try:
mgr = ContextManager()
except ValueError as err:
print('__init__ raised:', err)
else:
try:
value = type(mgr).__enter__(mgr)
except ValueError as err:
print('__enter__ raised:', err)
else:
exit = type(mgr).__exit__
exc = True
try:
try:
BLOCK
except TypeError:
pass
except:
exc = False
try:
exit_val = exit(mgr, *sys.exc_info())
except ValueError as err:
print('__exit__ raised:', err)
else:
if not exit_val:
raise
except ValueError as err:
print('BLOCK raised:', err)
finally:
if exc:
try:
exit(mgr, None, None, None)
except ValueError as err:
print('__exit__ raised:', err)
Di solito un approccio più semplice andrà bene
La necessità di una simile gestione delle eccezioni dovrebbe essere piuttosto rara e normalmente sarà sufficiente avvolgere il tutto with
in un try ... except
blocco. Soprattutto se le varie fonti di errore sono indicate da diversi tipi di eccezione (personalizzati) (i gestori di contesto devono essere progettati di conseguenza) possiamo facilmente distinguerli. Per esempio:
try:
with ContextManager():
BLOCK
except InitError: # raised from __init__
...
except AcquireResourceError: # raised from __enter__
...
except ValueError: # raised from BLOCK
...
except ReleaseResourceError: # raised from __exit__
...
with
dichiarazione non magicamente rompere un circostantetry...except
dichiarazione.