Quand et comment dois-je utiliser les exceptions?


20

Le réglage

J'ai souvent du mal à déterminer quand et comment utiliser les exceptions. Prenons un exemple simple: supposons que je racle une page Web, dites " http://www.abevigoda.com/ ", pour déterminer si Abe Vigoda est toujours en vie. Pour ce faire, il suffit de télécharger la page et de rechercher les moments où l'expression "Abe Vigoda" apparaît. Nous rendons la première apparition, car cela inclut le statut d'Abe. Conceptuellement, cela ressemblera à ceci:

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

parse_abe_status(s)prend une chaîne de la forme "Abe Vigoda est quelque chose " et renvoie la partie " quelque chose ".

Avant de prétendre qu'il existe des façons bien meilleures et plus robustes de gratter cette page pour le statut d'Abe, rappelez-vous que ce n'est qu'un exemple simple et artificiel utilisé pour mettre en évidence une situation courante dans laquelle je me trouve.

Maintenant, où ce code peut-il rencontrer des problèmes? Parmi d'autres erreurs, certaines "attendues" sont:

  • download_pagepeut ne pas être en mesure de télécharger la page et lance un IOError.
  • L'URL peut ne pas pointer vers la bonne page ou la page est téléchargée de manière incorrecte et il n'y a donc pas de résultats. hitsest la liste vide, alors.
  • La page Web a été modifiée, ce qui rend peut-être fausses nos hypothèses sur la page. Peut-être que nous attendons 4 mentions d'Abe Vigoda, mais maintenant nous en trouvons 5.
  • Pour certaines raisons, il hits[0]peut ne pas s'agir d'une chaîne de la forme "Abe Vigoda est quelque chose ", et elle ne peut donc pas être correctement analysée.

Le premier cas n'est pas vraiment un problème pour moi: un IOErrorest lancé et peut être géré par l'appelant de ma fonction. Examinons donc les autres cas et comment je pourrais les gérer. Mais d'abord, supposons que nous implémentions parse_abe_statusde la manière la plus stupide possible:

def parse_abe_status(s):
    return s[13:]

À savoir, il ne fait aucune vérification d'erreur. Maintenant, passons aux options:

Option 1: retour None

Je peux dire à l'appelant que quelque chose s'est mal passé en retournant None:

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    if not hits:
        return None

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

Si l'appelant reçoit Nonede ma fonction, il doit supposer qu'il n'y a pas eu de mention d'Abe Vigoda, et donc quelque chose s'est mal passé. Mais c'est assez vague, non? Et cela n'aide pas le cas où ce hits[0]n'est pas ce que nous pensions que c'était.

En revanche, nous pouvons prévoir quelques exceptions:

Option 2: utilisation d'exceptions

Si hitsest vide, un IndexErrorsera lancé lorsque nous tenterons hits[0]. Mais il ne faut pas s'attendre à ce que l'appelant gère une IndexErrorprojection de ma fonction, car il n'a aucune idée d'où cela IndexErrorvient; il aurait pu être jeté par find_all_mentions, pour tout ce qu'il sait. Nous allons donc créer une classe d'exception personnalisée pour gérer cela:

class NotFoundError(Exception):
    """Throw this when something can't be found on a page."""

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    try:
        hits[0]
    except IndexError:
        raise NotFoundError("No mentions found.")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

Maintenant, que se passe-t-il si la page a changé et qu'il y a un nombre inattendu de visites? Ce n'est pas catastrophique, car le code peut encore fonctionner, mais un appelant peut vouloir être supplémentaire prudent, ou il pourrait vouloir connecter un avertissement. Je vais donc lancer un avertissement:

class NotFoundError(Exception):
    """Throw this when something can't be found on a page."""

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    try:
        hits[0]
    except IndexError:
        raise NotFoundError("No mentions found.")

    # say we expect four hits...
    if len(hits) != 4:
        raise Warning("An unexpected number of hits.")
        logger.warning("An unexpected number of hits.")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

Enfin, nous pourrions trouver que ce statusn'est ni vivant ni mort. Peut-être, pour une raison étrange, aujourd'hui, cela s'est avéré être comatose. Ensuite, je ne veux pas revenir False, car cela implique qu'Abe est mort. Que dois-je faire ici? Jetez une exception, probablement. Mais quel genre? Dois-je créer une classe d'exception personnalisée?

class NotFoundError(Exception):
    """Throw this when something can't be found on a page."""

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    try:
        hits[0]
    except IndexError:
        raise NotFoundError("No mentions found.")

    # say we expect four hits...
    if len(hits) != 4:
        raise Warning("An unexpected number of hits.")
        logger.warning("An unexpected number of hits.")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    if status not in ['alive', 'dead']:
        raise SomeTypeOfError("Status is an unexpected value.")

    # he's either alive or dead
    return status == "alive"

Option 3: quelque part entre les deux

Je pense que la deuxième méthode, avec des exceptions, est préférable, mais je ne sais pas si j'utilise correctement les exceptions. Je suis curieux de voir comment des programmeurs plus expérimentés géreraient cela.

Réponses:


17

La recommandation en Python est d'utiliser des exceptions pour indiquer l'échec. Cela est vrai même si vous attendez régulièrement un échec.

Regardez-le du point de vue de l'appelant de votre code:

my_status = get_abe_status(my_url)

Et si nous retournons Aucun? Si l'appelant ne gère pas spécifiquement le cas où get_abe_status a échoué, il essaiera simplement de continuer avec my_stats étant None. Cela peut produire un bug difficile à diagnostiquer plus tard. Même si vous ne vérifiez aucun, ce code n'a aucune idée pourquoi get_abe_status () a échoué.

Et si nous levions une exception? Si l'appelant ne gère pas spécifiquement le cas, l'exception se propagera vers le haut et finira par atteindre le gestionnaire d'exceptions par défaut. Ce n'est peut-être pas ce que vous voulez, mais il vaut mieux introduire un bug subtil ailleurs dans le programme. De plus, l'exception donne des informations sur ce qui s'est mal passé qui est perdu dans la première version.

Du point de vue de l'appelant, il est tout simplement plus pratique d'obtenir une exception qu'une valeur de retour. Et c'est le style python, utiliser des exceptions pour indiquer que les conditions d'échec ne renvoient pas de valeurs.

Certains adopteront une perspective différente et soutiendront que vous ne devez utiliser des exceptions que pour les cas auxquels vous ne vous attendez pas vraiment. Ils soutiennent que le fonctionnement normal du fonctionnement ne devrait pas soulever d'exceptions. Une raison qui est donnée pour cela est que les exceptions sont extrêmement inefficaces, mais ce n'est pas réellement vrai pour Python.

Quelques points sur votre code:

try:
    hits[0]
except IndexError:
    raise NotFoundError("No mentions found.")

C'est une façon vraiment déroutante de vérifier une liste vide. N'induisez pas d'exception juste pour vérifier quelque chose. Utilisez un if.

# say we expect four hits...
if len(hits) != 4:
    raise Warning("An unexpected number of hits.")
    logger.warning("An unexpected number of hits.")

Vous vous rendez compte que la ligne logger.warning ne fonctionnera jamais correctement?


1
Merci (tardivement) pour votre réponse. Cela, en plus de regarder le code publié, a amélioré mon sentiment quant au moment et à la façon de lever une exception.
jme

4

La réponse acceptée mérite d'être acceptée et répond à la question, j'écris ceci uniquement pour fournir un peu de contexte supplémentaire.

Un des credos de Python est: il est plus facile de demander pardon que permission. Cela signifie qu'en général, vous ne faites que des choses et si vous attendez des exceptions, vous les gérez. Par opposition à faire si vérifie à l'avance pour vous assurer que vous n'obtiendrez pas d'exception.

Je veux fournir un exemple pour vous montrer à quel point la différence de mentalité avec C ++ / Java est dramatique. Une boucle for en C ++ ressemble généralement à quelque chose comme:

for(int i = 0; i != myvector.size(); ++i) ...

Une façon de penser à cela: accéder à myvector[k]où k> = myvector.size () provoquera une exception. Donc, vous pourriez en principe écrire cela (très maladroitement) comme un try-catch.

    for(int i = 0; ; ++i)  {
        try {
           ...
        } catch (& std::out_of_range)
             break

Ou quelque chose de similaire. Maintenant, considérez ce qui se passe dans une boucle python for:

for i in range(1):
    ...

Comment ça marche? La boucle for prend le résultat de range (1) et appelle iter () dessus, saisissant un itérateur.

b = range(1).__iter__()

Ensuite, il appelle ensuite à chaque itération de boucle, jusqu'à ce que ...:

>>> next(b)
0
>>> next(b)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

En d'autres termes, une boucle for en python est en fait un essai, sauf déguisé.

En ce qui concerne la question concrète, rappelez-vous que les exceptions arrêtent l'exécution normale des fonctions et doivent être traitées séparément. En Python, vous devez les lancer librement chaque fois qu'il n'y a aucun point à exécuter le reste du code dans votre fonction, et / ou qu'aucun des retours ne reflète correctement ce qui s'est passé dans la fonction. Notez que retourner tôt à partir d'une fonction est différent: retourner tôt signifie que vous avez déjà compris la réponse et n'avez pas besoin du reste du code pour comprendre la réponse. Je dis que des exceptions doivent être levées lorsque la réponse n'est pas connue, et le reste du code pour déterminer la réponse ne peut pas être raisonnablement exécuté. Maintenant, "refléter correctement" lui-même, comme les exceptions que vous choisissez de lever, est une question de documentation.

Dans le cas de votre code particulier, je dirais que toute situation qui fait que les hits soient une liste vide devrait être levée. Pourquoi? Eh bien, la façon dont votre fonction est configurée, il n'y a aucun moyen de déterminer la réponse sans analyser les hits. Donc, si les hits ne sont pas analysables, soit parce que l'URL est mauvaise, soit parce que les hits sont vides, alors la fonction ne peut pas répondre à la question, et en fait ne peut même pas vraiment essayer.

Dans ce cas particulier, je dirais que même si vous parvenez à analyser et n'obtenez pas de réponse raisonnable (vivante ou morte), vous devez quand même lancer. Pourquoi? Parce que la fonction renvoie un booléen. Aucun retour n'est très dangereux pour votre client. S'ils effectuent une vérification if sur Aucun, il n'y aura pas d'échec, il sera simplement traité en silence comme Faux. Ainsi, votre client devra fondamentalement toujours faire un test if is None de toute façon s'il ne veut pas d'échecs silencieux ... vous devriez donc probablement simplement jeter.


2

Vous devez utiliser des exceptions lorsque quelque chose d' exceptionnel se produit. Autrement dit, quelque chose qui ne devrait pas se produire étant donné l'utilisation appropriée de l'application. S'il est permis et attendu que le consommateur de votre méthode recherche quelque chose qui ne sera pas trouvé, alors "non trouvé" n'est pas un cas exceptionnel. Dans ce cas, vous devez retourner null ou "None" ou {}, ou quelque chose indiquant un ensemble de retours vide.

Si, d'autre part, vous vous attendez vraiment à ce que les consommateurs de votre méthode trouvent toujours (à moins qu'ils aient foiré d'une manière ou d'une autre) ce qui est recherché, alors ne pas le trouver serait une exception et vous devriez y aller.

La clé est que la gestion des exceptions peut être coûteuse - les exceptions sont censées recueillir des informations sur l'état de votre application lorsqu'elles se produisent, comme une trace de pile, afin d'aider les gens à déchiffrer pourquoi elles se sont produites. Je ne pense pas que c'est ce que vous essayez de faire.


1
Si vous décidez que ne pas trouver de valeur est autorisé, faites attention à ce que vous utilisez pour indiquer que c'est ce qui s'est produit. Si votre méthode est censée retourner un Stringet que vous choisissez "Aucun" comme indicateur, cela signifie que vous devez faire attention à ce que "Aucun" ne soit jamais une valeur valide. Notez également qu'il y a une différence entre regarder les données et ne pas trouver de valeur et ne pas pouvoir récupérer les données, donc nous ne pouvons pas trouver les données. Avoir le même résultat pour ces deux cas signifie que vous n'avez aucune visibilité une fois que vous n'obtenez aucune valeur lorsque vous vous attendez à ce qu'il y en ait un.
unholysampler

Les blocs de code en ligne sont marqués de guillemets (`), c'est peut-être ce que vous vouliez faire avec le" Aucun "?
Izkata

3
J'ai bien peur que ce soit absolument faux en Python. Vous appliquez un raisonnement de style C ++ / Java à un autre langage. Python utilise des exceptions pour indiquer la fin d'une boucle for; c'est assez exceptionnel.
Nir Friedman

2

Si j'écrivais une fonction

 def abe_is_alive():

Je l'écrirais dans return Trueou Falsedans les cas où je suis absolument certain de l'un ou de l'autre, et d' raiseune erreur dans tout autre cas (par exemple raise ValueError("Status neither 'dead' nor 'alive'")). C'est parce que la fonction qui appelle la mienne attend un booléen, et si je ne peux pas fournir cela avec certitude, le flux de programme normal ne devrait pas continuer.

Quelque chose comme votre exemple d'obtenir un nombre de "hits" différent de celui attendu, je l'ignorerais probablement; tant que l'un des hits correspond toujours à mon schéma "Abe Vigoda est {mort | vivant}", c'est très bien. Cela permet de réorganiser la page mais obtient toujours les informations appropriées.

Plutôt que

try:
    hits[0] 
except IndexError:
    raise NotFoundError

Je vérifierais explicitement:

if not hits:
    raise NotFoundError

car cela a tendance à être «moins cher», puis à configurer le try.

Je suis d'accord avec vous IOError; Je n'essaierais pas non plus de gérer la connexion au site Web - si nous ne pouvons pas, pour une raison quelconque, ce n'est pas l'endroit approprié pour le gérer (car cela ne nous aide pas à répondre à notre question) et cela devrait passer vers la fonction appelante.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.