web-dev-qa-db-fra.com

Meilleure pratique en python pour la valeur de retour sur erreur vs succès

Dans général, disons que vous avez une méthode comme celle ci-dessous.

def intersect_two_lists(self, list1, list2):
    if not list1:
        self.trap_error("union_two_lists: list1 must not be empty.")
        return False
    if not list2:
        self.trap_error("union_two_lists: list2 must not be empty.")
        return False
    #http://bytes.com/topic/python/answers/19083-standard
    return filter(lambda x:x in list1,list2)

Dans cette méthode particulière lorsque des erreurs sont trouvées, je ne voudrais pas retourner la liste vide dans ce cas car cela aurait pu être la vraie réponse à cet appel de méthode spécifique, je veux retourner quelque chose pour indiquer que les paramètres étaient incorrects. J'ai donc retourné False en cas d'erreur dans ce cas, et une liste sinon (vide ou non).

Ma question est, quelle est la meilleure pratique dans des domaines comme celui-ci, et pas seulement pour les listes? Renvoyez ce que je veux et assurez-vous de le documenter pour qu'un utilisateur puisse le lire? :-) Que font la plupart d'entre vous:

  1. Si en cas de succès, vous étiez censé retourner Vrai ou Faux et que vous avez attrapé une erreur?
  2. Si en cas de succès, vous étiez censé renvoyer une liste et que vous avez détecté une erreur?
  3. Si en cas de succès, vous étiez censé renvoyer un descripteur de fichier et que vous détectez une erreur?
  4. etc
48
TallPaul

Tout d'abord, quoi que vous fassiez ne renvoie pas de résultat ni de message d'erreur. C'est une très mauvaise façon de gérer les erreurs et vous causera des maux de tête sans fin. Si vous devez indiquer une erreur, déclenchez toujours une exception.

J'ai tendance à éviter les erreurs, sauf si cela est nécessaire. Dans votre exemple, lancer une erreur n'est pas vraiment nécessaire. L'intersection d'une liste vide avec une liste non vide n'est pas une erreur. Le résultat est juste une liste vide et c'est correct. Mais disons que vous voulez gérer d'autres cas. Par exemple, si la méthode a un type non liste. Dans ce cas, il est préférable de lever une exception. L'exception n'a rien à craindre.

Mon conseil pour vous est de regarder la bibliothèque Python pour des fonctions similaires et de voir comment Python gère ces cas spéciaux. Par exemple, jetez un œil à la méthode d'intersection dans l'ensemble, il a tendance à être indulgent. Ici, j'essaie de croiser un ensemble vide avec une liste vide:

>>> b = []
>>> a = set()
>>> a.intersection(b)
set([])

>>> b = [1, 2]
>>> a = set([1, 3])
>>> a.intersection(b)
set([1])

Les erreurs ne sont lancées qu'en cas de besoin:

>>> b = 1
>>> a.intersection(b)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'int' object is not iterable

Bien sûr, il y a des cas où retourner Vrai ou Faux en cas de succès ou d'échec peut être bon. Mais il est très important d'être cohérent. La fonction doit toujours renvoyer le même type ou la même structure. C'est très déroutant d'avoir une fonction qui pourrait retourner une liste ou un booléen. Ou renvoyez le même type mais la signification de cette valeur peut être différente en cas d'erreur.

MODIFIER:

Le PO dit:

Je veux retourner quelque chose pour indiquer que les paramètres étaient incorrects.

Rien ne dit qu'il y a mieux une erreur qu'une exception. Si vous souhaitez indiquer que les paramètres sont incorrects, utilisez des exceptions et mettez un message d'erreur utile. Renvoyer un résultat dans ce cas est tout simplement déroutant. Il pourrait y avoir d'autres cas où vous voudriez indiquer que rien ne s'est produit mais ce n'est pas une erreur. Par exemple, si vous disposez d'une méthode qui supprime des entrées d'une table et que l'entrée demandée pour suppression n'existe pas. Dans ce cas, il peut être judicieux de renvoyer simplement True ou False en cas de succès ou d'échec. Cela dépend de l'application et du comportement souhaité

59
Nadia Alramli

Il serait préférable de déclencher une exception que de renvoyer une valeur spéciale. C'est exactement pour cela que les exceptions ont été conçues, pour remplacer les codes d'erreur par un mécanisme de gestion des erreurs plus robuste et structuré.

class IntersectException(Exception):
    def __init__(self, msg):
        self.msg = msg
    def __str__(self):
        return self.msg

def intersect_two_lists(self, list1, list2):
    if not list1: raise IntersectException("list1 must not be empty.")
    if not list2: raise IntersectException("list2 must not be empty.")

    #http://bytes.com/topic/python/answers/19083-standard
    return filter(lambda x:x in list1,list2)

Dans ce cas précis, je laisserais probablement tomber les tests. Il n'y a rien de mal à croiser des listes vides, vraiment. De plus, lambda est en quelque sorte découragé de nos jours plutôt que de lister les compréhensions. Voir Trouver l'intersection de deux listes? pour quelques façons d'écrire ceci sans utiliser lambda.

21
John Kugelman

J'aime retourner un tuple:

(Vrai, un_résultat)

(Faux, some_useful_response)

L'objet some_useful_response peut être utilisé pour gérer la condition de retour ou peut servir à afficher des informations de débogage.

NOTE : cette technique s'applique pour valeurs de retour de toute sorte. Il ne faut pas le confondre avec cas d'exception.

Côté réception, il suffit de déballer:

Code, Response = some_function (...)

Cette technique s'applique au flux de contrôle "normal": il faut utiliser la fonctionnalité d'exception lorsque des entrées/traitements inattendus se produisent.

A noter également: cette technique permet de normaliser les retours de fonction. Le programmeur et l'utilisateur des fonctions savoir à quoi s'attendre.

AVERTISSEMENT: je viens d'un milieu Erlang :-)

13
jldupont

Les exceptions sont certainement meilleures (et plus Pythonic) que les retours d'état. Pour en savoir plus à ce sujet: Exceptions vs retours d'état

11
Ned Batchelder

Le cas général est de lever des exceptions pour des circonstances exceptionnelles. Je souhaite pouvoir me souvenir de la citation exacte (ou qui l'a dit), mais vous devez vous efforcer d'obtenir des fonctions qui acceptent autant de valeurs et de types qu'il est raisonnable et maintiennent un comportement très étroitement défini. Ceci est une variante de ce que Nadia parlait . Considérez les utilisations suivantes de votre fonction:

  1. intersect_two_lists(None, None)
  2. intersect_two_lists([], ())
  3. intersect_two_lists('12', '23')
  4. intersect_two_lists([1, 2], {1: 'one', 2: 'two'})
  5. intersect_two_lists(False, [1])
  6. intersect_two_lists(None, [1])

Je m'attendrais à ce que (5) lève une exception car le passage de False est une erreur de type. Les autres, cependant, ont un certain sens, mais cela dépend vraiment du contrat énoncé par la fonction. Si intersect_two_lists a été défini comme renvoyant l'intersection de deux itérables , alors tout autre que (5) devrait fonctionner tant que vous faites de None une représentation valide de ensemble vide . La mise en œuvre serait quelque chose comme:

def intersect_two_lists(seq1, seq2):
    if seq1 is None: seq1 = []
    if seq2 is None: seq2 = []
    if not isinstance(seq1, collections.Iterable):
        raise TypeError("seq1 is not Iterable")
    if not isinstance(seq2, collections.Iterable):
        raise TypeError("seq1 is not Iterable")
    return filter(...)

J'écris généralement des fonctions d'aide qui appliquent quel que soit le contrat, puis je les appelle pour vérifier toutes les conditions préalables. Quelque chose comme:

def require_iterable(name, arg):
    """Returns an iterable representation of arg or raises an exception."""
    if arg is not None:
        if not isinstance(arg, collections.Iterable):
            raise TypeError(name + " is not Iterable")
        return arg
    return []

def intersect_two_lists(seq1, seq2):
    list1 = require_iterable("seq1", seq1)
    list2 = require_iterable("seq2", seq2)
    return filter(...)

Vous pouvez également étendre ce concept et passer la "politique" comme argument facultatif. Je ne conseillerais pas de le faire à moins que vous ne vouliez adopter Design Based Policy . Je voulais le mentionner juste au cas où vous n'auriez pas étudié cette option auparavant.

Si le contrat pour intersect_two_lists est qu'il n'accepte que deux paramètres list non vides, puis être explicite et lever des exceptions si le contrat est rompu:

def require_non_empty_list(name, var):
    if not isinstance(var, list):
        raise TypeError(name + " is not a list")
    if var == []:
        raise ValueError(name + " is empty")

def intersect_two_lists(list1, list2):
    require_non_empty_list('list1', list1)
    require_non_empty_list('list2', list2)
    return filter(...)

Je pense que la morale de l'histoire est quoi que vous fassiez, faites-le de manière cohérente et explicite . Personnellement, je préfère généralement lever des exceptions chaque fois qu'un contrat est rompu ou que l'on me donne une valeur que je ne peux vraiment pas utiliser. Si les valeurs qui me sont données sont raisonnables, j'essaie de faire quelque chose de raisonnable en retour. Vous pouvez également lire les C++ FAQ Lite Lite sur les exceptions. Cette entrée particulière vous donne encore plus matière à réflexion sur les exceptions.

3
D.Shawley

Pour les collections (listes, ensembles, dict, etc.), le retour d'une collection vide est le choix évident car cela permet à la logique de votre site d'appel de rester à l'écart de la logique défensive. Plus explicitement, une collection vide est toujours une réponse parfaitement bonne d'une fonction à partir de laquelle vous attendez une collection, vous n'avez pas à vérifier que le résultat est de tout autre type et pouvez continuer votre logique métier de manière propre.

Pour les résultats non collectés, il existe plusieurs façons de gérer les retours conditionnels:

  1. Comme de nombreuses réponses l'ont déjà expliqué, l'utilisation d'exceptions est un moyen de résoudre ce problème, et c'est du python idiomatique. Cependant, je préfère ne pas utiliser d'exceptions pour le flux de contrôle car je trouve que cela crée une ambiguïté sur les intentions d'une exception. Au lieu de cela, soulevez des exceptions dans des situations exceptionnelles réelles.
  2. Une autre solution consiste à renvoyer None au lieu du résultat escompté, mais cela oblige un utilisateur à ajouter des contrôles défensifs partout dans ses sites d'appels, obscurcissant la logique métier réelle qu'ils essaient d'exécuter.
  3. Une troisième façon consiste à utiliser un type de collection qui ne peut contenir qu'un seul élément (ou être explicitement vide). Ceci est appelé facultatif et est ma méthode préférée car elle vous permet de garder une logique de site d'appel propre. Cependant, python n'a pas de type optionnel intégré, donc j'utilise le mien. Je l'ai publié dans une petite bibliothèque appelée optionnelle. py si quelqu'un veut l'essayer. Vous pouvez l'installer en utilisant pip install optional.py. J'accueille les commentaires, les demandes de fonctionnalités et les contributions.
1
Chad Befus