Differenziazione tra le possibili origini delle eccezioni sollevate da una withdichiarazione composta
La differenziazione tra le eccezioni che si verificano in withun'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 within a try .. exceptnon è sufficiente. Considera il seguente esempio (usando ValueErrorcome esempio ma ovviamente potrebbe essere sostituito con qualsiasi altro tipo di eccezione):
try:
with ContextManager():
BLOCK
except ValueError as err:
print(err)
Qui exceptcattureranno 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 withdell'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 BLOCKe __exit__perché withviene 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 exceptclausola generale nel corpo dell'archivio withper archiviare qualsiasi potenziale eccezione che altrimenti sarebbe sfuggita inosservata e confrontarla con quella rilevata in exceptseguito più esterna - se sono uguali ciò significa che l'origine era BLOCKo 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" withdell'istruzione. Qui possiamo facilmente avvolgere le varie parti con try ... excepte 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 within un try ... exceptblocco. 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__
...
withdichiarazione non magicamente rompere un circostantetry...exceptdichiarazione.