web-dev-qa-db-fra.com

Comment déterminer si une exception a été déclenchée une fois que vous êtes dans le bloc enfin?

Est-il possible de dire s'il y avait une exception une fois que vous êtes dans la clause finally? Quelque chose comme:

try:
    funky code
finally:
    if ???:
        print('the funky code raised')

Je cherche à rendre quelque chose comme ça plus SEC:

try:
    funky code
except HandleThis:
    # handle it
    raised = True
except DontHandleThis:
    raised = True
    raise
else:
    raised = False
finally:
    logger.info('funky code raised %s', raised)

Je n'aime pas que cela nécessite d'attraper une exception, que vous n'avez pas l'intention de gérer, juste pour définir un indicateur.


Puisque certains commentaires demandent moins de "M" dans le MCVE, voici un peu plus d'informations sur le cas d'utilisation. Le problème réel concerne l'escalade des niveaux de journalisation.

  • Le code funky est tiers et ne peut pas être modifié.
  • L'exception d'échec et la trace de pile ne contiennent aucune information de diagnostic utile, donc l'utilisation de logger.exception dans un bloc except n'est pas utile ici.
  • Si le code funky a soulevé, alors certaines informations que j'ai besoin de voir ont déjà été enregistrées, au niveau DEBUG. Nous ne gérons pas et ne pouvons pas gérer l'erreur, mais nous voulons intensifier la journalisation DEBUG car les informations nécessaires s'y trouvent.
  • Le code funky ne soulève pas, la plupart du temps. Je ne veux pas augmenter les niveaux de journalisation pour le cas général, car il est trop détaillé.

Par conséquent, le code s'exécute dans un contexte de capture de journal (qui configure des gestionnaires personnalisés pour intercepter les enregistrements de journal) et certaines informations de débogage sont retranscrites rétrospectivement:

try:
    with LogCapture() as log:
        funky_code()  # <-- third party badness
finally:
    mylog = mylogger.WARNING if <there was exception> else mylogger.DEBUG
    for record in log.captured:
        mylog(record.msg, record.args)
40
wim
raised = True
try:
    funky code
    raised = False
except HandleThis:
    # handle it
finally:
    logger.info('funky code raised %s', raised)

Étant donné les informations de base supplémentaires ajoutées à la question sur la sélection d'un niveau de journal, cela semble très facilement adapté au cas d'utilisation prévu:

mylog = WARNING
try:
    funky code
    mylog = DEBUG
except HandleThis:
    # handle it
finally:
    mylog(...)
31

Utilisation d'un gestionnaire de contexte

Vous pouvez utiliser un gestionnaire de contexte personnalisé, par exemple:

class DidWeRaise:
    __slots__ = ('exception_happened', )  # instances will take less memory

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        # If no exception happened the `exc_type` is None
        self.exception_happened = exc_type is not None

Et puis utilisez cela dans le try:

try:
    with DidWeRaise() as error_state:
        # funky code
finally:
    if error_state.exception_happened:
        print('the funky code raised')

C'est toujours une variable supplémentaire mais elle est probablement beaucoup plus facile à réutiliser si vous souhaitez l'utiliser à plusieurs endroits. Et vous n'avez pas besoin de le basculer vous-même.

Utiliser une variable

Dans le cas où vous ne voulez pas du gestionnaire de contexte, j'inverserais la logique du déclencheur et le basculerais seulement au cas où non une exception s'est produite. De cette façon, vous n'avez pas besoin d'un cas except pour les exceptions que vous ne voulez pas gérer. L'endroit le plus approprié serait la clause else qui est entrée au cas où le try n'a pas levé d'exception:

exception_happened = True
try:
    # funky code
except HandleThis:
    # handle this kind of exception
else:
    exception_happened = False
finally:
    if exception_happened:
        print('the funky code raised')

Et comme déjà indiqué au lieu d'avoir une variable "bascule", vous pouvez la remplacer (dans ce cas) par la fonction de journalisation souhaitée:

mylog = mylogger.WARNING
try:
    with LogCapture() as log:
        funky_code()
except HandleThis:
    # handle this kind of exception
else:
    # In case absolutely no exception was thrown in the try we can log on debug level
    mylog = mylogger.DEBUG
finally:
    for record in log.captured:
        mylog(record.msg, record.args)

Bien sûr, cela fonctionnerait aussi si vous le mettez à la fin de votre try (comme d'autres réponses suggérées ici) mais je préfère la clause else car elle a plus de sens ("ce code est destiné à exécuter uniquement s'il n'y avait pas d'exception dans le bloc try ") et peut être plus facile à gérer à long terme. Bien qu'il soit toujours plus à gérer que le gestionnaire de contexte, car la variable est définie et basculée à différents endroits.

En utilisant sys.exc_info (fonctionne uniquement pour les exceptions non gérées)

La dernière approche que je veux mentionner n'est probablement pas utile pour vous mais peut-être utile pour les futurs lecteurs qui veulent seulement savoir s'il y a une exception non gérée (une exception qui n'était pas pris dans un bloc except ou a été soulevé dans un bloc except). Dans ce cas, vous pouvez utiliser sys.exc_info :

import sys

try:
    # funky code
except HandleThis:
    pass
finally:
    if sys.exc_info()[0] is not None:
        # only entered if there's an *unhandled* exception, e.g. NOT a HandleThis exception
        print('funky code raised')
40
MSeifert

D'accord, donc à quoi cela ressemble, vous voulez juste modifier votre gestionnaire de contexte existant ou utiliser une approche similaire: logbook a en fait quelque chose appelé FingersCrossedHandler qui ferait exactement ce que vous voulez. Mais vous pouvez le faire vous-même, comme:

@contextmanager
def LogCapture():
    # your existing buffer code here
    level = logging.WARN
    try:
        yield
    except UselessException:
        level = logging.DEBUG
        raise  # Or don't, if you just want it to go away
    finally:
        # emit logs here

Réponse d'origine

Vous y pensez un peu de côté.

Vous faites avez l'intention de gérer l'exception - vous la gérez en définissant un indicateur. Peut-être que vous ne vous souciez de rien d'autre (ce qui semble être une mauvaise idée), mais si vous vous souciez de faire quelque chose lorsque an exception est levée, alors vous voulez être explicite à ce sujet.

Le fait que vous définissiez une variable, mais que vous souhaitez que l'exception continue, signifie que ce que vous vraiment voulez, c'est lever votre propre exception spécifique, à partir de l'exception qui a été levée:

class MyPkgException(Exception):  pass
class MyError(PyPkgException): pass  # If there's another exception type, you can also inherit from that

def do_the_badness():
    try:
        raise FileNotFoundError('Or some other code that raises an error')
    except FileNotFoundError as e:
        raise MyError('File was not found, doh!') from e
    finally:
        do_some_cleanup()

try:
    do_the_badness()
except MyError as e:
    print('The error? Yeah, it happened')

Cela résout:

  • Gestion explicite des exceptions que vous cherchez à gérer
  • Mise à disposition des traces de pile et des exceptions d'origine
  • Autoriser votre code qui va gérer l'exception d'origine ailleurs pour gérer votre exception levée
  • Permettre à un code de gestion des exceptions de niveau supérieur d'attraper simplement MyPkgException pour intercepter toutes vos exceptions afin qu'il puisse consigner quelque chose et quitter avec un statut Nice au lieu d'une trace de pile laide
3
Wayne Werner

Vous pouvez facilement affecter votre exception interceptée à une variable et l'utiliser dans le bloc finally, par exemple:

>>> x = 1
>>> error = None
>>> try:
...     x.foo()
... except Exception as e:
...     error = e
... finally:
...     if error is not None:
...             print(error)
...
'int' object has no attribute 'foo'
2
Jonathan R