Devolver un booleano cuando el éxito o el fracaso es la única preocupación

14

A menudo me encuentro devolviendo un booleano desde un método, que se usa en múltiples ubicaciones, para contener toda la lógica alrededor de ese método en un solo lugar. Todo lo que debe saber el método de llamada (interno) es si la operación fue exitosa o no.

Estoy usando Python, pero la pregunta no es necesariamente específica de ese idioma. Solo puedo pensar en dos opciones

  1. Genere una excepción, aunque las circunstancias no sean excepcionales, y recuerde que debe capturar esa excepción en todos los lugares donde se llama la función
  2. Devuelve un booleano como lo estoy haciendo.

Este es un ejemplo simple realmente que demuestra de qué estoy hablando.

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()

Aunque es funcional, realmente me disgusta esta forma de hacer algo, "huele" y, a veces, puede dar lugar a muchos ifs anidados. Pero, no puedo pensar en una forma más sencilla.

Podría recurrir a una filosofía más LBYL y usar os.path.exists(filename) antes de intentar la eliminación, pero no hay garantías de que el archivo no se haya bloqueado mientras tanto (es poco probable pero posible) y todavía tengo que determinar si la eliminación ha tenido éxito o no.

¿Es este un diseño "aceptable" y, si no, cuál sería una mejor manera de diseñar esto?

    
pregunta Ben 16.05.2013 - 18:29

4 respuestas

11

Debería devolver boolean cuando el método / función sea útil para tomar decisiones lógicas.

Debería lanzar un exception cuando no sea probable que se utilice el método / función en las decisiones lógicas.

Usted tiene que tomar una decisión sobre qué tan importante es la falla, y si debe manejarse o no. Si puede clasificar la falla como una advertencia, entonces devuelva boolean . Si el objeto entra en un estado malo que hace que las llamadas futuras sean inestables, arroje un exception .

Otra práctica es devolver objects en lugar de un resultado. Si llama a open , entonces debería devolver un objeto File o null si no se puede abrir. Esto garantiza que los programadores tengan una instancia de objeto que se encuentre en un estado válido que se pueda usar.

EDITAR:

Tenga en cuenta que la mayoría de los idiomas descartarán el resultado de una función cuando su tipo sea booleano o entero. Por lo tanto, es posible llamar a la función cuando no hay asignación de la mano izquierda para el resultado. Cuando trabaje con resultados booleanos, siempre asuma que el programador está ignorando el valor devuelto y úselo para decidir si debería ser una excepción.

    
respondido por el cgTag 16.05.2013 - 18:39
4

Tu intuición es correcta, hay una mejor manera de hacerlo: mónadas .

¿Qué son las mónadas?

Las mónadas son (para parafrasear a Wikipedia) una forma de encadenar operaciones mientras ocultan el mecanismo de encadenamiento; en su caso, el mecanismo de encadenamiento es el if s anidado. Oculta eso y tu código olerá mucho mejor .

Hay un par de mónadas que harán eso ("Tal vez" y "Cualquiera de los dos") y, por suerte, forman parte de una muy buena biblioteca de mónadas python!

Lo que pueden hacer por tu código

Este es un ejemplo que utiliza la mónada "Oither" ("Failable" en la biblioteca vinculada a), donde una función puede devolver un éxito o un error, dependiendo de lo que ocurrió:

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."))

Ahora, esto podría no ser muy diferente de lo que tienes ahora, pero considera cómo serían las cosas si tuvieras más operaciones que pudieran resultar en una falla:

    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."))

En cada uno de los yield s en la función process_file , si la llamada a la función devuelve una falla, la función process_file saldrá, en ese punto , devolviendo el valor de falla desde la función fallida, en lugar de continuar con el resto y devolver el Success("All ok.")

Ahora, ¡imagina hacer lo anterior con if s anidado! (¿Cómo manejarías el valor de retorno?)

Conclusión

Las mónadas son bonitas :)

Notas :

No soy un programador de Python: utilicé la biblioteca de mónadas enlazada anteriormente en un script que ninja'd para la automatización de algunos proyectos. Sin embargo, deduzco que, en general, el enfoque idiomático preferido es usar excepciones.

IIRC hay un error tipográfico en el script lib en la página enlazada, aunque olvido dónde está el cajero automático. Actualizaré si lo recuerdo. Diferencié mi versión con la de la página y encontré: def failable_monad_examle(): - > def failable_monad_example(): : faltaba el p en example .

Para obtener el resultado de una función decorada Failable (como process_file ), debes capturar el resultado en un variable y hacer un variable.value para obtenerlo.

    
respondido por el paul 16.05.2013 - 21:46
2

Una función es un contrato, y su nombre debe sugerir qué contrato cumplirá. En mi humilde opinión, si lo llama remove_file por lo que debería eliminar el archivo y no hacerlo podría causar una excepción. Por otro lado, si lo llama try_remove_file , debería "intentar" eliminar y devolver el valor booleano para saber si el archivo se ha eliminado o no.

Esto llevaría a otra pregunta: ¿debería ser remove_file o try_remove_file ? Depende de su sitio de llamada. En realidad, puede tener ambos métodos y usarlos en diferentes escenarios, pero creo que eliminar el archivo por sí mismo tiene muchas posibilidades de éxito, por lo que prefiero tener solo el remove_file que produce una excepción cuando falla.

    
respondido por el tia 16.05.2013 - 20:08
0

En este caso particular, puede ser útil pensar por qué es posible que no pueda eliminar el archivo. Digamos que el problema es que el archivo puede o no existir. Entonces deberías tener una función doesFileExist() que devuelva verdadero o falso, y una función removeFile() que solo elimine el archivo.

En su código, primero debe verificar si el archivo existe. Si lo hace, llame a removeFile . Si no, haz otras cosas.

En este caso, es posible que aún desee que removeFile produzca una excepción si el archivo no se puede eliminar por alguna otra razón, como los permisos.

Para resumir, se deben lanzar excepciones para cosas que son, bueno, excepcionales. Por lo tanto, si es perfectamente normal que el archivo que intenta eliminar no exista, no es una excepción. Escribe un predicado booleano para verificar eso. Por otro lado, si no tiene los permisos de escritura para el archivo, o si se encuentra en un sistema de archivos remoto que no se puede acceder de forma repentina, es muy posible que se trate de condiciones excepcionales.     

respondido por el Dima 16.05.2013 - 22:59

Lea otras preguntas en las etiquetas