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

14

Spesso mi ritrovo a restituire un booleano da un metodo, utilizzato in più posizioni, al fine di contenere tutta la logica attorno a quel metodo in un unico punto. Tutto il metodo di chiamata (interno) deve sapere se l'operazione ha avuto successo o meno.

Sto usando Python ma la domanda non è necessariamente specifica per quella lingua. Ci sono solo due opzioni a cui posso pensare

  1. Solleva un'eccezione, anche se le circostanze non sono eccezionali e ricorda di rilevare quell'eccezione in ogni punto in cui la funzione è chiamata
  2. Restituisce un booleano come sto facendo.

Questo è un semplice esempio che dimostra ciò di cui 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 a volte può generare un sacco di if annidati. Ma, non riesco a pensare a un modo più semplice.

Potrei passare a una filosofia LBYL e usare 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 no.

Si tratta di un design "accettabile" e, in caso contrario, quale sarebbe un modo migliore di progettarlo?

    
posta Ben 16.05.2013 - 18:29
fonte

4 risposte

11

Dovresti restituire boolean quando il metodo / funzione è utile per prendere decisioni logiche.

Dovresti lanciare un exception quando non è probabile che il metodo / la funzione venga utilizzata 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 un avvertimento, quindi restituire boolean . Se l'oggetto entra in uno stato negativo che rende future le chiamate ad esso instabili, quindi lancia un exception .

Un'altra pratica è di restituire objects invece di un risultato. Se chiami open , allora dovrebbe restituire un oggetto File o null se impossibile aprire. Ciò garantisce che i programmatori abbiano un'istanza di oggetto che si trova in uno stato valido che può essere utilizzato.

EDIT:

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

    
risposta data 16.05.2013 - 18:39
fonte
4

Il tuo intuito su questo è corretto, c'è un modo migliore per farlo: monadi .

Cosa sono le monadi?

Le monadi sono (per parafrasare Wikipedia) un modo di concatenare le operazioni insieme nascondendo il meccanismo di concatenamento; nel tuo caso il meccanismo di concatenamento è il% nidificatoif s. Nascondilo e il tuo codice avrà un odore molto più bello.

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

Cosa possono fare per il tuo codice

Ecco un esempio che utilizza la monade "Either" ("Failable" nella libreria collegata a), in cui una funzione può restituire un esito positivo o negativo, a seconda di cosa si è verificato:

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 ciascuno dei yield s nella funzione process_file , se la chiamata alla funzione restituisce un errore, la funzione process_file uscirebbe, a quel punto , restituendo il valore Failure dalla funzione fallita, invece di continuare con il resto e restituire Success("All ok.")

Ora, immagina di fare quanto sopra con% nidificatoif! (Come gestiresti il valore di ritorno!?)

Conclusione

Le Monade sono belle:)

Note:

Non sono un programmatore Python - Ho usato la libreria monad collegata in precedenza in uno script che ho usato per l'automazione di alcuni progetti. Tuttavia, ritengo che in generale l'approccio idiomatico preferito sia l'uso delle eccezioni.

IIRC c'è un errore di battitura nello script lib nella pagina collegata a se ho dimenticato dove si trova ATM. Aggiornerò se ricordo. Ho diffto la mia versione rispetto alla pagina e ho trovato: def failable_monad_examle(): - > def failable_monad_example(): - mancava il p in example .

Per ottenere il risultato di una funzione decorata F (come process_file ) devi catturare il risultato in variable e fare un variable.value per ottenerlo.

    
risposta data 16.05.2013 - 21:46
fonte
2

Una funzione è un contratto e il suo nome dovrebbe suggerire quale contratto adempierà. IMHO, se lo chiami remove_file quindi 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 booleano per sapere se il file è stato rimosso o meno.

Ciò porterebbe a un'altra domanda: dovrebbe essere remove_file o 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 di file per sé abbia alte probabilità di successo, quindi preferisco avere solo remove_file che genera un'eccezione quando fallisce.

    
risposta data 16.05.2013 - 20:08
fonte
0

In questo caso particolare può essere utile riflettere sul motivo per cui potresti non essere in grado di rimuovere il file. Diciamo che il problema è che il file può o non può esistere. Quindi dovresti avere una funzione doesFileExist() che restituisce true o false e una funzione removeFile() che elimina il file.

Nel tuo codice devi prima controllare se il file esiste. In tal caso, chiama removeFile . In caso contrario, quindi fare altre cose.

In questo caso potresti ancora voler removeFile per generare un'eccezione se il file non può essere rimosso per qualche altro motivo, come le autorizzazioni.

Per riassumere, le eccezioni dovrebbero essere gettate per cose che sono, beh, eccezionali. Quindi, se è perfettamente normale che il file che stai cercando di eliminare possa non esistere, allora quella 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 che è improvvisamente inaccessibile, queste potrebbero essere condizioni eccezionali.

    
risposta data 16.05.2013 - 22:59
fonte

Leggi altre domande sui tag