Zwracanie wartości logicznej, gdy jedynym celem jest sukces lub porażka

15

Często zwracam wartość logiczną z metody używanej w wielu lokalizacjach, aby zawrzeć całą logikę wokół tej metody w jednym miejscu. Wszystkie (wewnętrzne) metody wywoływania muszą wiedzieć, czy operacja się powiodła, czy nie.

Używam Pythona, ale pytanie niekoniecznie jest specyficzne dla tego języka. Są tylko dwie opcje, o których mogę myśleć

  1. Zgłaszaj wyjątek, chociaż okoliczności nie są wyjątkowe, i pamiętaj, aby wychwycić ten wyjątek w każdym miejscu, w którym wywoływana jest funkcja
  2. Zwróć wartość logiczną, tak jak ja.

To jest naprawdę prosty przykład, który pokazuje, o czym mówię.

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

Chociaż jest funkcjonalny, naprawdę nie podoba mi się ten sposób robienia czegoś, „pachnie”, a czasami może powodować wiele zagnieżdżonych ifów. Ale nie mogę wymyślić prostszego sposobu.

Mógłbym przejść do bardziej filozofii LBYL i użyć go os.path.exists(filename)przed próbą usunięcia, ale nie ma gwarancji, że plik nie zostanie w międzyczasie zablokowany (jest mało prawdopodobne, ale możliwe) i nadal muszę ustalić, czy usunięcie zakończyło się powodzeniem, czy nie.

Czy jest to „akceptowalny” projekt, a jeśli nie, jaki byłby lepszy sposób zaprojektowania tego?

Ben
źródło

Odpowiedzi:

11

Powinieneś powrócić, booleangdy metoda / funkcja jest przydatna w podejmowaniu logicznych decyzji.

Powinieneś rzucić, exceptionkiedy metoda / funkcja prawdopodobnie nie będzie używana w logicznych decyzjach.

Musisz podjąć decyzję, jak ważna jest awaria i czy należy ją naprawić, czy nie. Jeśli możesz zaklasyfikować awarię jako ostrzeżenie, wróć boolean. Jeśli obiekt wejdzie w zły stan, który spowoduje, że przyszłe wywołania będą niestabilne, to rzuć exception.

Inną praktyką jest powrót objectszamiast wyniku. Jeśli zadzwonisz open, powinien zwrócić Fileobiekt lub nulljeśli nie można go otworzyć. Dzięki temu programiści mają instancję obiektu, która jest w poprawnym stanie, z którego można korzystać.

EDYTOWAĆ:

Należy pamiętać, że większość języków odrzuci wynik funkcji, gdy jej typ jest typu logicznego lub całkowitego. Można więc wywołać tę funkcję, gdy nie ma przypisania lewej ręki dla wyniku. Podczas pracy z wynikami logicznymi zawsze zakładaj, że programista ignoruje zwracaną wartość i użyj tej, aby zdecydować, czy raczej powinien to być wyjątek.

Reactgular
źródło
To potwierdzenie tego, co robię, więc podoba mi się odpowiedź :-). Na obiektach, choć rozumiem, skąd pochodzisz, nie widzę, jak to pomaga w większości przypadków, gdybym z niego korzystał. Chcę być SUCHY, więc będę ponownie stroił obiekt do jednej metody, ponieważ chcę zrobić tylko jedną rzecz. Został mi wtedy ten sam kod, który mam teraz, z wyjątkiem dodatkowej metody. (również w podanym przykładzie usuwam plik, więc obiekt o pustym pliku nie mówi zbyt wiele :-)
Ben
Usuwanie jest trudne, ponieważ nie jest gwarantowane. Nigdy nie widziałem, aby metoda usuwania plików generowała wyjątek, ale co programista może zrobić, jeśli się nie powiedzie? Ciągłe powtarzanie pętli? Nie, to problem z systemem operacyjnym. Kod powinien zapisać wynik i przejść dalej.
Reactgular
4

Twoja intuicja jest prawidłowa, istnieje lepszy sposób: monady .

Co to są monady?

Monady to (parafrazując Wikipedię) sposób łączenia operacji łańcuchowych przy jednoczesnym ukrywaniu mechanizmu łączenia; w twoim przypadku mechanizmem łańcuchowym są zagnieżdżone ifs. Ukryj że i Twój kod będzie zapach dużo ładniejszy.

Jest kilka monad, które to zrobią („Może” i „Albo”) i na szczęście są częścią naprawdę ładnej biblioteki monad Pythona!

Co mogą zrobić dla twojego kodu?

Oto przykład z użyciem monady „Albo” („Niepowodzenie” w połączonej bibliotece), w której funkcja może zwrócić sukces lub niepowodzenie, w zależności od tego, co się wydarzyło:

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

Teraz może to nie wyglądać inaczej niż teraz, ale zastanów się, jak by to było, gdybyś miał więcej operacji, które mogłyby spowodować awarię:

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

W przypadku każdego yields process_filefunkcji, jeśli wywołanie funkcji zwróci błąd, wówczas process_filefunkcja wyjdzie w tym momencie , zwracając wartość błędu z funkcji błędu, zamiast kontynuować przez resztę i zwracającSuccess("All ok.")

A teraz wyobraź sobie robienie powyższego z zagnieżdżonymi literami ifs! (Jak poradziłbyś sobie z wartością zwracaną !?)

Wniosek

Monady są fajne :)


Uwagi:

Nie jestem programistą Python - użyłem biblioteki monad, do której odsyłam, w skrypcie, którego ninja użyłem do automatyzacji projektu. Rozumiem jednak, że ogólnie preferowanym, idiomatycznym podejściem jest stosowanie wyjątków.

IIRC zawiera literówkę w skrypcie lib na stronie, do której prowadzi link, choć nie pamiętam, gdzie jest ATM. Zaktualizuję, jeśli pamiętam. I diff'd moją wersję na stronę i stwierdził: def failable_monad_examle():-> def failable_monad_example():- w pw examplebrakowało.

Aby uzyskać wynik funkcji dekorowania awaryjnego (np. process_file), Musisz uchwycić wynik w a variablei zrobić, variable.valueaby go uzyskać.

Paweł
źródło
2

Funkcja jest umową, a jej nazwa powinna sugerować, jaką umowę będzie wypełniać. IMHO, jeśli nadasz mu nazwę, remove_filepowinien usunąć plik, a jego brak powinien spowodować wyjątek. Z drugiej strony, jeśli go nazwiesz try_remove_file, powinien „spróbować” usunąć i zwrócić wartość logiczną, aby stwierdzić, czy plik został usunięty, czy nie.

Prowadziłoby to do innego pytania - powinien być remove_filealbo try_remove_file? To zależy od Twojej strony połączeń. W rzeczywistości możesz mieć obie metody i używać ich w różnych scenariuszach, ale myślę, że usunięcie pliku jako takiego ma duże szanse powodzenia, więc wolę mieć tylko remove_fileten wyjątek rzucania, gdy się nie powiedzie.

tia
źródło
0

W tym konkretnym przypadku warto zastanowić się, dlaczego usunięcie pliku może nie być możliwe. Powiedzmy, że problem polega na tym, że plik może istnieć lub nie. Powinieneś mieć funkcję doesFileExist()zwracającą wartość true lub false oraz funkcję, removeFile()która po prostu usuwa plik.

W swoim kodzie najpierw sprawdzisz, czy plik istnieje. Jeśli tak, zadzwoń removeFile. Jeśli nie, zrób inne rzeczy.

W takim przypadku nadal removeFilemożesz zgłosić wyjątek, jeśli pliku nie można usunąć z innego powodu, na przykład uprawnień.

Podsumowując, należy rzucać wyjątki na rzeczy, które są, cóż, wyjątkowe. Jeśli więc całkowicie normalne jest to, że plik, który próbujesz usunąć, może nie istnieć, nie jest to wyjątkiem. Napisz predykat boolowski, aby to sprawdzić. Z drugiej strony, jeśli nie masz uprawnień do zapisu dla pliku lub jeśli znajduje się on w zdalnym systemie plików, który nagle jest niedostępny, mogą to być bardzo wyjątkowe warunki.

Dima
źródło
Jest to bardzo specyficzne dla podanego przeze mnie przykładu, którego wolałbym uniknąć. Jeszcze tego nie napisałem, będzie archiwizować pliki i rejestrować fakt, że tak się stało w bazie danych. Pliki można ponownie załadować w dowolnym momencie (chociaż po wczytaniu plików istnieje mniejsze prawdopodobieństwo ponownego załadowania) istnieje możliwość zablokowania pliku przez inny proces między sprawdzaniem a próbą usunięcia. Nie ma nic wyjątkowego w niepowodzeniu. Standardowo Python nie zawraca sobie głowy sprawdzaniem i wychwyceniem wyjątku po zgłoszeniu (jeśli jest to wymagane), tym razem po prostu nie chcę nic z tym zrobić.
Ben
Jeśli awaria nie jest niczym wyjątkowym, sprawdzenie, czy można usunąć plik, jest uzasadnioną częścią logiki programu. Zasada pojedynczej odpowiedzialności nakazuje, abyś miał funkcję sprawdzania i funkcję removeFile.
Dima