Question

Pourquoi les sous-classes virtuelles d'un résumé Exception créé à l'aide de la ABCMeta.register ne correspondent-elles pas sous la clause except?

Contexte

Je voudrais m'assurer que les exceptions générées par un package que j'utilise sont converties en MyException, afin que le code qui importe mon module puisse intercepter toute exception que mon module lève en utilisant except MyException: au lieu de except Exception afin qu'ils n'aient pas à dépendre d'un détail d'implémentation (le fait que j'utilise un package tiers).

Exemple

Pour ce faire, j'ai essayé d'enregistrer un OtherException en tant que MyException en utilisant une classe de base abstraite:

# Tested with python-3.6
from abc import ABC

class MyException(Exception, ABC):
    pass

class OtherException(Exception):
    """Other exception I can't change"""
    pass

MyException.register(OtherException)

assert issubclass(OtherException, MyException)  # passes

try:
    raise OtherException("Some OtherException")
except MyException:
    print("Caught MyException")
except Exception as e:
    print("Caught Exception: {}".format(e))

L'assertion passe (comme prévu), mais l'exception tombe dans le deuxième bloc:

Caught Exception: Some OtherException
3
Mike McCoy 12 avril 2018 à 19:16

4 réponses

Meilleure réponse

D'accord, j'ai approfondi la question. La réponse est que c'est un problème ouvert en suspens depuis longtemps dans Python3 (là depuis la toute première version) et apparemment, il a été le premier signalé en 2011. Comme Guido l'a dit dans les commentaires, "je suis d'accord que c'est un bogue et devrait être corrigé." Malheureusement, ce bogue a persisté en raison de préoccupations concernant les performances du correctif et de certains cas d'angle qui doivent être traités.

Le problème principal est que la routine de correspondance d'exceptions PyErr_GivenExceptionMatches dans { {X1}} utilise PyType_IsSubtype et non PyObject_IsSubclass. Comme les types et les objets sont censés être les mêmes en python3, cela équivaut à un bogue.

J'ai créé un PR vers python3 qui semble couvrir tous les problèmes abordés dans le fil, mais étant donné l'histoire, je ne suis pas super optimiste, cela va bientôt fusionner. Nous verrons.

3
Mike McCoy 14 avril 2018 à 16:23

Eh bien, cela ne répond pas vraiment à votre question directement, mais si vous essayez de vous assurer qu'un bloc de code appelle votre exception, vous pouvez adopter une stratégie différente en interceptant avec un gestionnaire de contexte.

In [78]: class WithException:
    ...:     
    ...:     def __enter__(self):
    ...:         pass
    ...:     def __exit__(self, exc, msg, traceback):
    ...:         if exc is OtherException:
    ...:             raise MyException(msg)
    ...:         

In [79]: with WithException():
    ...:     raise OtherException('aaaaaaarrrrrrggggh')
    ...: 
---------------------------------------------------------------------------
OtherException                            Traceback (most recent call last)
<ipython-input-79-a0a23168647e> in <module>()
      1 with WithException():
----> 2     raise OtherException('aaaaaaarrrrrrggggh')

OtherException: aaaaaaarrrrrrggggh

During handling of the above exception, another exception occurred:

MyException                               Traceback (most recent call last)
<ipython-input-79-a0a23168647e> in <module>()
      1 with WithException():
----> 2     raise OtherException('aaaaaaarrrrrrggggh')

<ipython-input-78-dba8b409a6fd> in __exit__(self, exc, msg, traceback)
      5     def __exit__(self, exc, msg, traceback):
      6         if exc is OtherException:
----> 7             raise MyException(msg)
      8 

MyException: aaaaaaarrrrrrggggh
0
snakes_on_a_keyboard 12 avril 2018 à 16:41

Le pourquoi est simple:

from abc import ABC

class MyException(Exception, ABC):
    pass

class OtherException(Exception):
    """Other exception I can't change"""
    pass

MyException.register(OtherException)

assert issubclass(OtherException, MyException)  # passes
assert OtherException in MyException.__subclasses__()  # fails

Edit: cette assert imite le résultat de la clause except, mais ne représente pas ce qui se passe réellement. Regardez la acceptez la réponse pour une explication.

La solution de contournement est également simple:

class OtherException(Exception):
    pass
class AnotherException(Exception):
    pass

MyException = (OtherException, AnotherException)
4
MegaIng 15 avril 2018 à 19:43

Il semble que CPython prenne à nouveau quelques raccourcis et ne prenne pas la peine d'appeler les Méthode __instancecheck__ pour les classes répertoriées dans les clauses except.

Nous pouvons tester cela en implémentant une métaclasse personnalisée avec __instancecheck__ et __subclasscheck__ méthodes:

class OtherException(Exception):
    pass

class Meta(type):
    def __instancecheck__(self, value):
        print('instancecheck called')
        return True

    def __subclasscheck__(self, value):
        print('subclasscheck called')
        return True

class MyException(Exception, metaclass=Meta):
    pass

try:
    raise OtherException("Some OtherException")
except MyException:
    print("Caught MyException")
except Exception as e:
    print("Caught Exception: {}".format(e))

# output:
# Caught Exception: Some OtherException

Nous pouvons voir que les instructions print dans la métaclasse ne sont pas exécutées.


Je ne sais pas si c'est un comportement voulu / documenté ou non. La chose la plus proche des informations pertinentes que j'ai pu trouver provient du tutoriel de gestion des exceptions:

Une classe dans une clause except est compatible avec une exception s'il s'agit de la même classe ou d'une classe de base de celle-ci

Cela signifie-t-il que les classes doivent être des sous-classes réelles (c'est-à-dire que la classe parente doit faire partie du MRO de la sous-classe)? Je ne sais pas.


En ce qui concerne une solution de contournement: vous pouvez simplement faire de MyException un alias de OtherException.

class OtherException(Exception):
    pass

MyException = OtherException

try:
    raise OtherException("Some OtherException")
except MyException:
    print("Caught MyException")
except Exception as e:
    print("Caught Exception: {}".format(e))

# output:
# Caught MyException

Dans le cas où vous devez intercepter plusieurs exceptions différentes qui n'ont pas de classe de base commune, vous pouvez définir MyException comme un tuple:

MyException = (OtherException, AnotherException)
1
Aran-Fey 12 avril 2018 à 17:06