Restituire un valore booleano quando il successo o il fallimento è l'unica preoccupazione


15

Mi ritrovo spesso a restituire un valore booleano da un metodo, utilizzato in più posizioni, al fine di contenere tutta la logica attorno a quel metodo in un unico posto. Tutto il metodo di chiamata (interno) deve sapere se l'operazione è andata a buon fine oppure no.

Sto usando Python ma la domanda non è necessariamente specifica per quella lingua. Ci sono solo due opzioni che mi vengono in mente

  1. Solleva un'eccezione, sebbene le circostanze non siano eccezionali e ricorda di cogliere quell'eccezione in ogni luogo in cui viene chiamata la funzione
  2. Restituisci un booleano mentre lo sto facendo.

Questo è un esempio davvero semplice che dimostra di cosa sto parlando.

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

Sebbene sia funzionale, non mi piace molto questo modo di fare qualcosa, "odora" e talvolta può provocare molti if annidati. Ma non riesco a pensare a un modo più semplice.

Potrei passare a una filosofia LBYL più e utilizzare os.path.exists(filename)prima di tentare la cancellazione, ma non ci sono garanzie che il file non sia stato bloccato nel frattempo (è improbabile ma possibile) e devo ancora determinare se la cancellazione ha avuto successo o meno.

È un design "accettabile" e se no quale sarebbe un modo migliore di progettarlo?

Risposte:


11

Dovresti tornare booleanquando il metodo / funzione è utile per prendere decisioni logiche.

Dovresti lanciare un exceptionquando il metodo / funzione non è probabile che venga utilizzato nelle decisioni logiche.

Devi prendere una decisione su quanto sia importante l'errore e se debba essere gestito o meno. Se è possibile classificare l'errore come avviso, quindi tornare boolean. Se l'oggetto entra in uno stato negativo che rende instabili le chiamate future, quindi lancia un exception.

Un'altra pratica è quella di restituire objectsinvece di un risultato. Se chiami open, dovrebbe restituire un Fileoggetto o nullse non è possibile aprirlo. Ciò garantisce che i programmatori abbiano un'istanza di oggetto in uno stato valido che può essere utilizzata.

MODIFICARE:

Tieni presente che la maggior parte delle lingue scarterà il risultato di una funzione quando il suo tipo è booleano o intero. Quindi è possibile chiamare la funzione quando non c'è assegnazione della mano sinistra per il risultato. Quando si lavora con risultati booleani, supporre sempre che il programmatore ignori il valore restituito e utilizzarlo per decidere se dovrebbe piuttosto essere un'eccezione.


È una convalida di ciò che sto facendo, quindi mi piace la risposta :-). Sugli oggetti, anche se capisco da dove vieni, non vedo come questo aiuti nella maggior parte dei casi lo userei. Voglio essere ASCIUTTO, quindi sto per risintonizzare l'oggetto con un solo metodo poiché voglio solo fare una cosa. Mi rimane quindi lo stesso codice che ho ora, salva con un metodo aggiuntivo. (anche per l'esempio dato sto eliminando il file quindi un oggetto file null non dice molto :-)
Ben

L'eliminazione è complicata, perché non è garantita. Non ho mai visto un metodo di eliminazione dei file generare un'eccezione, ma cosa può fare il programmatore se fallisce? Nuovo tentativo continuo? No, è un problema con il sistema operativo. Il codice dovrebbe registrare il risultato e andare avanti.
Reactgular

4

La tua intuizione su questo è corretta, c'è un modo migliore per farlo: monadi .

Cosa sono le monadi?

Le monadi sono (per parafrasare Wikipedia) un modo per concatenare operazioni insieme nascondendo il meccanismo di concatenamento; nel tuo caso il meccanismo di concatenamento è il ifs nidificato . Nascondilo e il tuo codice avrà un profumo molto più gradevole.

Ci sono un paio di monadi che faranno proprio questo ("Forse" e "Entrambi") e fortunatamente per te fanno parte di una libreria monade di pitone davvero bella!

Cosa possono fare per il tuo codice

Ecco un esempio usando la monade "Oither" ("Disponibile" nella libreria collegata a), dove una funzione può restituire un successo o un fallimento, a seconda di ciò che è accaduto:

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

Ora, questo potrebbe non sembrare molto diverso da quello che hai ora, ma considera come sarebbero le cose se avessi più operazioni che potrebbero causare un errore:

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

A ciascuna delle yields nella process_filefunzione, se la chiamata di funzione restituisce un errore, la process_filefunzione uscirà, a quel punto , restituendo il valore di errore dalla funzione non riuscita, invece di continuare per il resto e restituire ilSuccess("All ok.")

Ora, immagina di fare quanto sopra con ifs nidificati ! (Come gestiresti il ​​valore di ritorno !?)

Conclusione

Le monadi sono belle :)


Appunti:

Non sono un programmatore Python - ho usato la libreria di monade collegata sopra in uno script che ho usato per l'automazione di un progetto. Ritengo, tuttavia, che in generale l'approccio idiomatico preferito sia usare le eccezioni.

IIRC c'è un refuso nello script lib sulla pagina collegato anche se dimentico dove si trova ATM. Aggiornerò se ricordo. Ho diff'd mia versione contro la pagina ed ho trovato: def failable_monad_examle():-> def failable_monad_example():- l' pin examplemancava.

Per ottenere il risultato di una funzione Decorata disponibile (come process_file) è necessario acquisire il risultato in a variablee fare un variable.valueper ottenerlo.


2

Una funzione è un contratto e il suo nome dovrebbe suggerire quale contratto adempirà. IMHO, se lo chiami remove_filecosì dovrebbe rimuovere il file e non farlo dovrebbe causare un'eccezione. D'altra parte, se lo chiami try_remove_file, dovrebbe "provare" a rimuovere e restituire il valore booleano per dire se il file è stato rimosso o meno.

Ciò porterebbe a un'altra domanda: dovrebbe essere remove_fileo try_remove_file? Dipende dal tuo sito di chiamata. In realtà, puoi avere entrambi i metodi e usarli in scenari diversi, ma penso che la rimozione dei file di per sé abbia grandi probabilità di successo, quindi preferisco avere solo remove_filequell'eccezione di lancio quando fallisce.


0

In questo caso particolare può essere utile pensare al motivo per cui potresti non essere in grado di rimuovere il file. Supponiamo che il problema sia che il file potrebbe esistere o meno. Quindi dovresti avere una funzione doesFileExist()che restituisce vero o falso e una funzione removeFile()che cancella semplicemente il file.

Nel tuo codice verifichi prima se il file esiste. In tal caso, chiama removeFile. In caso contrario, fai altre cose.

In questo caso, potresti voler removeFilelanciare un'eccezione se il file non può essere rimosso per qualche altro motivo, ad esempio le autorizzazioni.

Per riassumere, dovrebbero essere generate eccezioni per cose che sono, beh, eccezionali. Quindi, se è perfettamente normale che il file che si sta tentando di eliminare potrebbe non esistere, questa non è un'eccezione. Scrivi un predicato booleano per verificarlo. D'altra parte, se non si dispone delle autorizzazioni di scrittura per il file o se si trova su un file system remoto improvvisamente inaccessibile, è possibile che si tratti di condizioni eccezionali.


Questo è molto specifico nell'esempio che ho dato, che preferirei evitare. Non l'ho ancora scritto, archivierà i file e registrerà il fatto che ciò è accaduto nel database. I file possono essere ricaricati in qualsiasi momento (anche se una volta che i file caricati hanno molte meno probabilità di essere ricaricati) è possibile che un file possa essere bloccato da un altro processo tra il controllo e il tentativo di eliminazione. Non c'è niente di eccezionale in un fallimento. Python standard non si preoccupa prima di controllare e coglie l'eccezione quando viene sollevato (se necessario), semplicemente non voglio farci niente questa volta.
Ben

Se non c'è nulla di eccezionale nell'errore, verificare se è possibile rimuovere un file è una parte legittima della logica del programma. Il principio di responsabilità singola impone che si debba disporre di una funzione di controllo e di una funzione removeFile.
Dima,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.