Retourner un booléen lorsque le succès ou l'échec est la seule préoccupation

15

Je me retrouve souvent à renvoyer un booléen à partir d'une méthode, qui est utilisée à plusieurs endroits, afin de contenir toute la logique autour de cette méthode en un seul endroit. Tout ce que la méthode d'appel (interne) doit savoir, c'est si l'opération a réussi ou non.

J'utilise Python mais la question n'est pas nécessairement spécifique à ce langage. Il n'y a que deux options auxquelles je peux penser

  1. Lève une exception, bien que les circonstances ne soient pas exceptionnelles, et souviens-toi d'attraper cette exception à chaque endroit où la fonction est appelée
  2. Retourne un booléen comme je le fais.

Ceci est un exemple vraiment simple qui montre de quoi je parle.

import os

class DoSomething(object):

    def remove_file(self, filename):

        try:
            os.remove(filename)
        except OSError:
            return False

        return True

    def process_file(self, filename):

        do_something()

        if remove_file(filename):
            do_something_else()

Bien qu'il soit fonctionnel, je n'aime vraiment pas cette façon de faire quelque chose, il "sent" et peut parfois donner lieu à de nombreux if imbriqués. Mais, je ne peux pas penser à un moyen plus simple.

Je pourrais me tourner vers une philosophie et une utilisation plus LBYL os.path.exists(filename)avant d'essayer de supprimer, mais il n'y a aucune garantie que le fichier n'aura pas été verrouillé entre-temps (c'est peu probable mais possible) et je dois encore déterminer si la suppression a réussi ou non.

Est-ce une conception "acceptable" et sinon, quelle serait une meilleure façon de concevoir cela?

Ben
la source

Réponses:

11

Vous devez revenir booleanlorsque la méthode / fonction est utile pour prendre des décisions logiques.

Vous devez lancer un exceptionlorsque la méthode / fonction n'est pas susceptible d'être utilisée dans les décisions logiques.

Vous devez décider de l’importance de l’échec et s’il doit être traité ou non. Si vous pouvez classer l'échec comme un avertissement, revenez boolean. Si l'objet entre dans un mauvais état qui rend instables les appels futurs, lancez un exception.

Une autre pratique consiste à revenir objectsau lieu d'un résultat. Si vous appelez open, il devrait renvoyer un Fileobjet ou nulls'il ne peut pas s'ouvrir. Cela garantit que les programmeurs ont une instance d'objet dans un état valide qui peut être utilisé.

ÉDITER:

Gardez à l'esprit que la plupart des langues rejetteront le résultat d'une fonction lorsque son type est booléen ou entier. Il est donc possible d'appeler la fonction lorsqu'il n'y a pas d'affectation à gauche pour le résultat. Lorsque vous travaillez avec des résultats booléens, supposez toujours que le programmeur ignore la valeur renvoyée et utilisez-la pour décider si elle doit plutôt être une exception.

Reactgular
la source
C'est une validation de ce que je fais donc j'aime la réponse :-). Sur les objets, bien que je comprenne d'où vous venez, je ne vois pas comment cela aide dans la plupart des cas, je l'utiliserais. Je veux être SEC, je vais donc réaccorder l'objet à une seule méthode car je ne veux y faire qu'une chose. Je me retrouve alors avec le même code que j'ai maintenant, enregistrez avec une méthode supplémentaire. (également pour l'exemple donné, je supprime le fichier afin qu'un objet de fichier nul ne dise pas grand-chose :-)
Ben
La suppression est délicate, car elle n'est pas garantie. Je n'ai jamais vu une méthode de suppression de fichier lever une exception, mais que peut faire le programmeur en cas d'échec? Nouvelle tentative de boucle en continu? Non, c'est un problème de système d'exploitation. Le code doit enregistrer le résultat et continuer.
Reactgular
4

Votre intuition à ce sujet est correcte, il existe une meilleure façon de le faire: les monades .

Que sont les monades?

Les monades sont (pour paraphraser Wikipedia) un moyen de chaîner des opérations ensemble tout en cachant le mécanisme de chaînage; dans votre cas, le mécanisme de chaînage est le ifs imbriqué . Cachez cela et votre code sentira beaucoup plus agréable.

Il y a quelques monades qui feront exactement cela ("Peut-être" et "Soit") et heureusement pour vous, elles font partie d' une bibliothèque de monades en python vraiment sympa!

Ce qu'ils peuvent faire pour votre code

Voici un exemple utilisant la monade "Soit" ("Disponible" dans la bibliothèque liée à), où une fonction peut retourner un succès ou un échec, selon ce qui s'est produit:

import os

class DoSomething(object):

    def remove_file(self, filename):
        try:
            os.remove(filename)
            return Success(None)
        except OSError:
            return Failure("There was an OS Error.")

    @do(Failable)
    def process_file(self, filename):
        do_something()
        yield remove_file(filename)
        do_something_else()
        mreturn(Success("All ok."))

Maintenant, cela pourrait ne pas être très différent de ce que vous avez maintenant, mais considérez comment les choses se passeraient si vous aviez plus d'opérations pouvant entraîner un échec:

    def action_that_might_fail_and_returns_something(self):
        # get some random value between 0 and 1 here
        if value < 0.5:
            return Success(value)
        else:
            return Failure("Bad value! Bad! Go to your room!")

    @do(Failable)
    def process_file(self, filename):
        do_something()
        yield remove_file(filename)
        yield action_that_might_fail(somearg)
        yield another_action_that_might_fail(someotherarg)
        some_val = yield action_that_might_fail_and_returns_something()
        yield something_that_used_the_return_value(some_val)
        do_something_else()
        mreturn(Success("All ok."))

À chacun des yields de la process_filefonction, si l'appel de fonction renvoie un échec, la process_filefonction quittera, à ce stade , en retournant la valeur d'échec de la fonction en échec, au lieu de continuer tout au long du reste et de retourner leSuccess("All ok.")

Maintenant, imaginez faire ce qui précède avec des ifs imbriqués ! (Comment géreriez-vous la valeur de retour!?)

Conclusion

Les monades sont sympa :)


Remarques:

Je ne suis pas un programmeur Python - j'ai utilisé la bibliothèque monade liée à ci-dessus dans un script que j'ai ninja pour une certaine automatisation de projet. Je suppose, cependant, qu'en général l'approche privilégiée et idiomatique consiste à utiliser des exceptions.

IIRC il y a une faute de frappe dans le script lib sur la page liée à bien que j'oublie où il est ATM. Je mettrai à jour si je me souviens. Je diff'd ma version contre la page de et trouvé: def failable_monad_examle():-> def failable_monad_example():- l' pen exampleétait absent.

Afin d'obtenir le résultat d'une fonction décorée disponible (comme process_file), vous devez capturer le résultat dans un variableet faire un variable.valuepour l'obtenir.

Paul
la source
2

Une fonction est un contrat, et son nom doit suggérer quel contrat elle remplira. À mon humble avis, si vous le nommez remove_file, il devrait supprimer le fichier et à défaut, cela devrait provoquer une exception. D'un autre côté, si vous le nommez try_remove_file, il devrait "essayer" de supprimer et de retourner booléen pour savoir si le fichier a été supprimé ou non.

Cela mènerait à une autre question - devrait-il être remove_fileou try_remove_file? Cela dépend de votre site d'appel. En fait, vous pouvez avoir les deux méthodes et les utiliser dans des scénarios différents, mais je pense que la suppression du fichier en soi a de grandes chances de succès, je préfère donc n'avoir que remove_filecette exception de rejet en cas d'échec.

tia
la source
0

Dans ce cas particulier, il peut être utile de réfléchir aux raisons pour lesquelles vous ne pourrez peut-être pas supprimer le fichier. Disons que le problème est que le fichier peut exister ou non. Ensuite, vous devez avoir une fonction doesFileExist()qui renvoie vrai ou faux, et une fonction removeFile()qui supprime simplement le fichier.

Dans votre code, vous devez d'abord vérifier si le fichier existe. Si c'est le cas, appelez removeFile. Sinon, faites d'autres choses.

Dans ce cas, vous souhaiterez peut-être toujours removeFilelever une exception si le fichier ne peut pas être supprimé pour une autre raison, telle que des autorisations.

Pour résumer, des exceptions devraient être levées pour des choses qui sont, bien, exceptionnelles. Donc, s'il est parfaitement normal que le fichier que vous essayez de supprimer n'existe pas, ce n'est pas une exception. Écrivez un prédicat booléen pour vérifier cela. D'un autre côté, si vous ne disposez pas des autorisations d'écriture pour le fichier, ou s'il se trouve sur un système de fichiers distant qui est soudainement inaccessible, ces conditions peuvent très bien être exceptionnelles.

Dima
la source
C'est très spécifique à l'exemple que j'ai donné, que je préfère éviter. Je n'ai pas encore écrit ceci, il va archiver des fichiers et enregistrer le fait que cela s'est produit dans la base de données. Les fichiers peuvent être rechargés à tout moment (bien qu'une fois que les fichiers chargés sont beaucoup moins susceptibles d'être rechargés), il est possible qu'un fichier puisse être verrouillé par un autre processus entre la vérification et la tentative de suppression. Un échec n'a rien d'exceptionnel. Il est standard de Python pour ne pas prendre la peine de vérifier en premier et d'attraper l'exception lorsqu'il est déclenché (si nécessaire), je ne veux tout simplement rien faire avec cette fois.
Ben
S'il n'y a rien d'exceptionnel à l'échec, vérifier si vous pouvez supprimer un fichier est une partie légitime de la logique de votre programme. Le principe de responsabilité unique dicte que vous devez avoir une fonction de vérification et une fonction removeFile.
Dima