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
4 réponses
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.
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
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)
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)