Étendre dynamiquement l'exception avec une exception personnalisée en Python?
Disons que j'ai une exception spéciale qui fait des choses intéressantes et merveilleuses - Résoudre la faim dans le monde, la bonne volonté envers les hommes, l'exploitation forestière, etc.
class SpecialException(Exception):
# Does something really neat here.
pass
Disons maintenant qu'une exception peut être déclenchée, mais nous ne savons pas quel type d'exception nous rencontrerons:
def crashAndBurn():
try:
import random
choice = random.choice([1,2])
if choice == 1:
int('asdf') # Can't cast string to int: ValueError.
else:
x # Variable `x` doesn't exist: NameError.
except Exception as e:
# Code to wrap `e` into `SpecialException` class
raise e
Lorsque ce type d'exception inconnu est déclenché, nous voulons l'attraper, l'envelopper dans notre SpecialExceptionclasse et le lever afin qu'il puisse être intercepté soit par le type d'origine d'exception levée, soit en interceptant SpecialException:
try:
crashAndBurn()
except ValueError as e:
print('This time it was a ValueError, and we want to handle this differently for some reason')
except SpecialException as e:
print('Handle this more generically')
Des recommandations sur la façon de résoudre (raisonnablement) cela?
En résumé, nous avons besoin de:
- Capacité à intercepter l'exception soulevée à l'origine et capacité à intercepter
SpecialException - Conserver le traçage pour un débogage facile
- Contenir le message d'erreur de l'exception d'origine
Ce que j'ai essayé:
J'ai essayé d'utiliser raise SpecialException from e. Alors que nous sommes en mesure d'afficher le message d'erreur et retraçage de l'exception soulevée à l' origine, nous ne sommes plus en mesure de l' attraper par le type d'exception à l' origine jeté ... Par exemple: Nous pouvons attraper SpecialException, mais ne peut pas attraper ValueError:
def crashAndBurn():
try:
int('asdf') # ValueError
except Exception as e:
raise SpecialException('Special Exception Encountered').with_traceback(e.__traceback__) from e
try:
crashAndBurn()
except ValueError as e:
print('This will never be encountered')
except SpecialException as e:
print('This will be encountered, when we wanted it to be handled above')
Le plus proche que nous avons obtenu répond techniquement à nos besoins, cependant:
- Bien que l'exception déclenchée puisse être interceptée en tant que
SpecialExceptionouValueError, elle est en fait déclenchée comme une autre classe à usage unique:DynamicSpecialException - C'est vraiment dégoûtant et apparemment très peu pythonique
def crashAndBurn():
try:
int('asdf') # ValueError
except Exception as e:
class DynamicSpecialException(SpecialException, e.__class__):
pass # I feel dirty
raise DynamicSpecialException('Special Exception Encountered').with_traceback(e.__traceback__)
try:
crashAndBurn()
except ValueError as e:
print('Caught as a ValueError!')
try:
crashAndBurn()
except SpecialException as e:
print('Caught as SpecialException!')
Ce que je m'attendais vraiment à trouver, c'était quelque chose de similaire à raise e.extend(SpecialException)ou raise SpecialException.from(e)- plutôt que ce terrier de lapin que j'ai apparemment déçu aujourd'hui! :)
Réponses
Voici un petit coup d'œil. Il semble faire la plupart de ce que vous voulez, sauf qu'il ajoute la trace de pile de la gestion de l'usine spéciale.
Ce que j'ai appris, c'est que vous ne pouvez pas échanger la classe d'exception e.__class__ = <dynamic class>, vous devez en créer une nouvelle et l'augmenter.
import pdb
from traceback import print_exc as xp
import sys
def cpdb():
""" put `pdb` on commmand line to halt execution in debugger """
return "pdb" in sys.argv
class SpecialException(Exception):
def solve_world_hunger(self):
print(f"eat more 🦄")
def specialfactory(e):
""" creates a class dynamically and keeps the original as a base"""
cls_ = type("SpecialException", (SpecialException, e.__class__),{})
e2 = cls_(str(e))
e2.ori = e
e2.__dict__.update(**e.__dict__)
# 👇 you can try different flavors to see what changes:
# basically, I haven't found a way to suppress `The above exception was the direct cause of the following exception:`
# see also https://stackoverflow.com/questions/33809864/disable-exception-chaining-in-python-3
# return e2
# raise e2. raise specialfactory(e).with_traceback(e.__traceback__) from e
# raise e2 from e
raise e2.with_traceback(e.__traceback__) from e
def crashAndBurn(no_special=False, custom_message=None):
try:
if custom_message:
exc = ValueError(custom_message)
exc.foo = "bar"
raise exc
int('asdf') # ValueError
except Exception as e:
if no_special:
#just to investigate what things look like for a plain ValueError
raise
# raise specialfactory(e).with_traceback(e.__traceback__) from e
raise specialfactory(e) from e
#################################################################
# check what a regular unchanged ValueError looks like
#################################################################
try:
print("\n\n\n🔬regular ValueError, unchanged")
crashAndBurn(no_special=1)
except ValueError as e:
if cpdb(): pdb.set_trace()
print(f' plain ValueError: {e}')
xp()
except SpecialException as e:
if cpdb(): pdb.set_trace()
print(f' Caught as a SpecialException!: {e}')
xp()
#################################################################
# catch a Special as a ValueError
#################################################################
try:
print("\n\n\n🔬ValueError ")
crashAndBurn()
except ValueError as e:
if cpdb(): pdb.set_trace()
print(f' Caught as a ValueError! {e}')
xp()
except SpecialException as e:
if cpdb(): pdb.set_trace()
print(f' Caught as a SpecialException! {e}')
xp()
#################################################################
# catch a Special
#################################################################
try:
print("\n\n\n🔬SpecialException handling")
crashAndBurn()
except SpecialException as e:
if cpdb(): pdb.set_trace()
print(f' Caught as a SpecialException! {e} {e.solve_world_hunger()}')
xp()
except ValueError as e:
if cpdb(): pdb.set_trace()
print(f' Caught as a ValueError! {e}')
xp()
#################################################################
# custom variables are still available
#################################################################
try:
print("\n\n\n🔬ValueError with custom_message/content ")
crashAndBurn(custom_message="my custom_message")
except SpecialException as e:
if cpdb(): pdb.set_trace()
print(f' Caught as a SpecialException! {e} {e.foo=} {e.solve_world_hunger()}')
xp()
except ValueError as e:
if cpdb(): pdb.set_trace()
print(f' Caught as a ValueError! {e}')
xp()
production:
Traceback (most recent call last):
File "test_183.py", line 57, in <module>
crashAndBurn(no_special=1)
File "test_183.py", line 41, in crashAndBurn
int('asdf') # ValueError
ValueError: invalid literal for int() with base 10: 'asdf'
Traceback (most recent call last):
File "test_183.py", line 41, in crashAndBurn
int('asdf') # ValueError
ValueError: invalid literal for int() with base 10: 'asdf'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "test_183.py", line 74, in <module>
crashAndBurn()
File "test_183.py", line 47, in crashAndBurn
raise specialfactory(e) from e
File "test_183.py", line 30, in specialfactory
raise e2.with_traceback(e.__traceback__) from e
File "test_183.py", line 41, in crashAndBurn
int('asdf') # ValueError
SpecialException: invalid literal for int() with base 10: 'asdf'
Traceback (most recent call last):
File "test_183.py", line 41, in crashAndBurn
int('asdf') # ValueError
ValueError: invalid literal for int() with base 10: 'asdf'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "test_183.py", line 92, in <module>
crashAndBurn()
File "test_183.py", line 47, in crashAndBurn
raise specialfactory(e) from e
File "test_183.py", line 30, in specialfactory
raise e2.with_traceback(e.__traceback__) from e
File "test_183.py", line 41, in crashAndBurn
int('asdf') # ValueError
SpecialException: invalid literal for int() with base 10: 'asdf'
Traceback (most recent call last):
File "test_183.py", line 39, in crashAndBurn
raise exc
ValueError: my custom_message
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "test_183.py", line 108, in <module>
crashAndBurn(custom_message="my custom_message")
File "test_183.py", line 47, in crashAndBurn
raise specialfactory(e) from e
File "test_183.py", line 30, in specialfactory
raise e2.with_traceback(e.__traceback__) from e
File "test_183.py", line 39, in crashAndBurn
raise exc
SpecialException: my custom_message
🔬regular ValueError, unchanged
plain ValueError: invalid literal for int() with base 10: 'asdf'
🔬ValueError
Caught as a ValueError! invalid literal for int() with base 10: 'asdf'
🔬SpecialException handling
eat more 🦄
Caught as a SpecialException! invalid literal for int() with base 10: 'asdf' None
🔬ValueError with custom_message/content
eat more 🦄
Caught as a SpecialException! my custom_message e.foo='bar' None