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:
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é
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
.
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 :-)
Les exceptions sont certainement meilleures (et plus Pythonic) que les retours d'état. Pour en savoir plus à ce sujet: Exceptions vs retours d'état
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:
intersect_two_lists(None, None)
intersect_two_lists([], ())
intersect_two_lists('12', '23')
intersect_two_lists([1, 2], {1: 'one', 2: 'two'})
intersect_two_lists(False, [1])
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.
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:
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.pip install optional.py
. J'accueille les commentaires, les demandes de fonctionnalités et les contributions.