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

15

A menudo me encuentro devolviendo un booleano de 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 necesita 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 lenguaje. Solo hay dos opciones en las que puedo pensar

  1. Genere una excepción, aunque las circunstancias no son excepcionales, y recuerde detectar esa excepción en cada lugar donde se llama la función
  2. Devuelve un booleano como lo estoy haciendo.

Este es un ejemplo realmente simple que demuestra de lo que 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 no me gusta 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 simple.

Podría recurrir a una filosofía y uso más LBYL 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 se realizó correctamente o no.

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

Ben
fuente

Respuestas:

11

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

Debería arrojar un exceptioncuando no es probable que el método / función se use en decisiones lógicas.

Debe tomar una decisión sobre la importancia de la falla y si debe manejarse o no. Si puede clasificar la falla como una advertencia, luego regrese boolean. Si el objeto entra en un mal estado que hace que las futuras llamadas sean inestables, entonces arroje un exception.

Otra práctica es regresar en objectslugar de un resultado. Si llama open, debería devolver un Fileobjeto o nullsi no puede abrirlo. Esto garantiza que los programadores tengan una instancia de objeto que se encuentre en un estado válido que pueda usarse.

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 mano izquierda para el resultado. Cuando trabaje con resultados booleanos, suponga siempre que el programador ignora el valor devuelto y úselo para decidir si debería ser una excepción.

Reactgular
fuente
Es una validación de lo que estoy haciendo, así que me gusta la respuesta :-). En los objetos, aunque entiendo de dónde vienes, no veo cómo esto ayuda en la mayoría de los casos, lo usaría. Quiero estar SECO, así que voy a volver a ajustar el objeto a un solo método, ya que solo quiero hacerle una cosa. Luego me queda el mismo código que tengo ahora, guardar con un método adicional. (también para el ejemplo dado, estoy borrando el archivo, por lo que un objeto de archivo nulo no dice mucho :-)
Ben
Eliminar es complicado, porque no está garantizado. Nunca he visto que un método de eliminación de archivos arroje una excepción, pero ¿qué puede hacer el programador si falla? Reintentar continuamente el bucle? No, es un problema del sistema operativo. El código debe registrar el resultado y seguir adelante.
Reactgular
4

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

¿Qué son las mónadas?

Las mónadas son (parafraseando a Wikipedia) una forma de encadenar operaciones juntas mientras se oculta el mecanismo de encadenamiento; en su caso, el mecanismo de encadenamiento es el ifs anidado . Esconda eso y su código tendrá un olor mucho más agradable.

¡Hay un par de mónadas que harán exactamente eso ("Quizás" y "Cualquiera") y por suerte para ti son parte de una muy buena biblioteca de mónadas de pitón!

Lo que pueden hacer por tu código

Aquí hay un ejemplo usando la mónada "Cualquiera" ("Disponible" en la biblioteca vinculada a), donde una función puede devolver un Éxito o Fallo, 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 verse muy diferente de lo que tiene ahora, pero considere cómo serían las cosas si tuviera 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 una de las yields de la process_filefunción, si la llamada a la función devuelve un Fallo, la process_filefunción saldrá, en ese punto , devolverá el valor de Fallo de la función fallida, en lugar de continuar por el resto y devolver elSuccess("All ok.")

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

Conclusión

Las mónadas son buenas :)


Notas:

No soy un programador de Python: utilicé la biblioteca de mónada vinculada anteriormente en un script que ninja había usado para la automatización de algunos proyectos. Sin embargo, deduzco que, en general, el enfoque idiomático preferido es utilizar excepciones.

IIRC hay un error tipográfico en el script lib en la página vinculada, aunque se me olvida dónde está ATM. Actualizaré si lo recuerdo. Me diff'd mi versión contra la página de y encontré: def failable_monad_examle():-> def failable_monad_example():- la pde exampleque faltaba.

Para obtener el resultado de una función decorada Failable (como process_file), debe capturar el resultado en ay variablehacer un variable.valuepara obtenerlo.

Pablo
fuente
2

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

Esto llevaría a otra pregunta: ¿debería ser remove_fileo try_remove_file? Depende de su sitio de llamadas. En realidad, puede tener ambos métodos y usarlos en diferentes escenarios, pero creo que eliminar el archivo per-se tiene una alta probabilidad de éxito, por lo que prefiero tener solo remove_fileesa excepción de lanzamiento cuando falla.

tia
fuente
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ía tener una función doesFileExist()que devuelva verdadero o falso, y una función removeFile()que simplemente elimine el archivo.

En su código, primero verificará si el archivo existe. Si es así, llame removeFile. Si no, entonces haz otras cosas.

En este caso, es posible que aún desee removeFilelanzar una excepción si el archivo no se puede eliminar por algún otro motivo, como los permisos.

Para resumir, se deben lanzar excepciones para cosas que son, bueno, excepcionales. Entonces, si es perfectamente normal que el archivo que está tratando de eliminar no exista, entonces eso no es una excepción. Escriba un predicado booleano para verificar eso. Por otro lado, si no tiene los permisos de escritura para el archivo, o si está en un sistema de archivos remoto que de repente es inaccesible, esas podrían ser condiciones excepcionales.

Dima
fuente
Eso es muy específico del ejemplo que he dado, que prefiero evitar. Todavía no he escrito esto, va a archivar archivos y registrar el hecho de que esto ha sucedido en la base de datos. Los archivos se pueden volver a cargar en cualquier momento (aunque una vez que los archivos cargados son mucho menos propensos a volver a cargarse) es posible que un archivo pueda ser bloqueado por otro proceso entre la verificación y el intento de eliminación. No hay nada excepcional en un fracaso. Es Python estándar no molestarse en verificar primero y detectar la excepción cuando se activa (si es necesario), esta vez simplemente no quiero hacer nada.
Ben
Si no hay nada excepcional sobre la falla, entonces verificar si puede eliminar un archivo es una parte legítima de la lógica de su programa. El principio de responsabilidad única dicta que debe tener una función de verificación y una función removeFile.
Dima