Ausnahme dynamisch mit benutzerdefinierter Ausnahme in Python erweitern?

Oct 05 2020

Nehmen wir an, ich habe eine besondere Ausnahme, die einige nette und wundervolle Dinge tut - Welthunger, guten Willen gegenüber Männern, Holzeinschlag usw.:

class SpecialException(Exception):
    # Does something really neat here.
    pass

Nehmen wir nun an, dass eine Ausnahme ausgelöst werden kann, aber wir wissen nicht, auf welche Art von Ausnahme wir stoßen werden:

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

Wenn dieser unbekannte SpecialExceptionAusnahmetyp ausgelöst wird, möchten wir ihn abfangen, in unsere Klasse einschließen und auslösen, damit er entweder vom ursprünglich ausgelösten Ausnahmetyp oder vom Fang abgefangen werden kann 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')

Irgendwelche Empfehlungen, wie dies (vernünftigerweise) gelöst werden kann?

Zusammenfassend brauchen wir:

  • Fähigkeit, ursprünglich ausgelöste Ausnahmen zu fangen, und Fähigkeit, zu fangen SpecialException
  • Behalten Sie den Traceback für ein einfaches Debuggen bei
  • Enthält die Fehlermeldung der ursprünglichen Ausnahme

Was ich versucht habe:

Versucht mit raise SpecialException from e. Während wir die Fehlermeldung und den Traceback der ursprünglich ausgelösten Ausnahme anzeigen können, können wir sie nicht mehr anhand des ursprünglich ausgelösten Ausnahmetyps abfangen ... Beispiel: Wir können abfangen SpecialException, aber nicht abfangen 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')

Der nächste, den wir technisch erreicht haben, erfüllt jedoch unsere Anforderungen:

  1. Während die ausgelöste Ausnahme als entweder SpecialExceptionoder abgefangen werden kann ValueError, wird sie tatsächlich als eine andere einmalige Verwendungsklasse ausgelöst:DynamicSpecialException
  2. Es ist wirklich eklig und scheinbar sehr unpythonisch
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!')

Was ich wirklich erwartet hatte, war etwas Ähnliches raise e.extend(SpecialException)oder raise SpecialException.from(e)- und nicht dieses Kaninchenloch, das ich heute scheinbar nach unten gewackelt habe! :) :)

Antworten

1 JLPeyret Oct 07 2020 at 21:19

Hier ist ein kleiner Stich drauf. Es scheint zu tun , die meisten von dem, was Sie wollen, außer dass es anhängt den Stack - Trace der specialfactory Handhabung.

Ich habe gelernt, dass Sie die Ausnahmeklasse nicht austauschen können e.__class__ = <dynamic class>. Sie müssen eine neue erstellen und sie auslösen.

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()

Ausgabe:

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