Czy dobrą praktyką jest używanie try-wyjątkiem-else w Pythonie?

439

Od czasu do czasu w Pythonie widzę blok:

try:
   try_this(whatever)
except SomeException as exception:
   #Handle exception
else:
   return something

Jaki jest powód istnienia try-else-else?

Nie podoba mi się tego rodzaju programowanie, ponieważ używa wyjątków do sterowania przepływem. Jeśli jednak jest to uwzględnione w języku, musi istnieć dobry powód, prawda?

Rozumiem, że wyjątki nie są błędami i powinny być używane tylko w wyjątkowych warunkach (np. Próbuję zapisać plik na dysku i nie ma już miejsca, a może nie mam uprawnień), a nie przepływu kontrola.

Zwykle obsługuję wyjątki, takie jak:

something = some_default_value
try:
    something = try_this(whatever)
except SomeException as exception:
    #Handle exception
finally:
    return something

Lub jeśli naprawdę nie chcę niczego zwracać, jeśli wystąpi wyjątek, wówczas:

try:
    something = try_this(whatever)
    return something
except SomeException as exception:
    #Handle exception
Juan Antonio Gomez Moriano
źródło

Odpowiedzi:

666

„Nie wiem, czy to z powodu ignorancji, ale nie podoba mi się tego rodzaju programowanie, ponieważ wykorzystuje wyjątki do kontroli przepływu”.

W świecie Python stosowanie wyjątków do kontroli przepływu jest powszechne i normalne.

Nawet programiści Python używają wyjątków do kontroli przepływu, a ten styl jest mocno upakowany w języku (tj. Protokół iteratora używa StopIteration do zakończenia pętli sygnału).

Ponadto zastosowano styl try-wyjątkiem, aby zapobiec warunkom wyścigowym związanym z niektórymi konstrukcjami „patrz przed skokiem” . Na przykład testowanie os.path.exists daje informacje, które mogą być nieaktualne do czasu ich użycia. Podobnie Queue.full zwraca informacje, które mogą być nieaktualne. W takich przypadkach styl try-else-else będzie generował bardziej niezawodny kod.

„Rozumiem, że wyjątki nie są błędami, powinny być stosowane tylko w wyjątkowych warunkach”

W niektórych innych językach zasada ta odzwierciedla ich normy kulturowe odzwierciedlone w ich bibliotekach. „Reguła” częściowo opiera się również na względach wydajnościowych dla tych języków.

Norma kulturowa Pythona jest nieco inna. W wielu przypadkach musisz użyć wyjątków dla kontroli przepływu. Ponadto użycie wyjątków w Pythonie nie spowalnia otaczającego kodu i wywoływania kodu, jak ma to miejsce w niektórych skompilowanych językach (tj. CPython już implementuje kod do sprawdzania wyjątków na każdym etapie, niezależnie od tego, czy faktycznie używasz wyjątków, czy nie).

Innymi słowy, twoje zrozumienie, że „wyjątki dotyczą wyjątków”, jest zasadą, która ma sens w niektórych innych językach, ale nie w Pythonie.

„Jeśli jednak jest ono zawarte w samym języku, musi istnieć dobry powód, prawda?”

Oprócz unikania warunków wyścigowych, wyjątki są również bardzo przydatne do wyciągania obsługi błędów poza pętlami. Jest to niezbędna optymalizacja w językach interpretowanych, które zwykle nie mają automatycznego ruchu niezmiennego w pętli .

Ponadto wyjątki mogą znacznie uprościć kod w typowych sytuacjach, w których zdolność do obsługi problemu jest daleka od miejsca powstania problemu. Na przykład powszechny jest kod wywołujący kod interfejsu użytkownika najwyższego poziomu dla logiki biznesowej, który z kolei wywołuje procedury niskiego poziomu. Sytuacje powstające w procedurach niskiego poziomu (takich jak zduplikowane rekordy unikatowych kluczy w dostępach do bazy danych) mogą być obsługiwane tylko w kodzie najwyższego poziomu (np. Proszenie użytkownika o nowy klucz, który nie powoduje konfliktu z istniejącymi kluczami). Zastosowanie wyjątków dla tego rodzaju przepływu sterowania pozwala procedurom średniego poziomu całkowicie zignorować problem i być ładnie oddzielonym od tego aspektu sterowania przepływem.

Jest tam fajny post na blogu o niezbędności wyjątków .

Zobacz także odpowiedź Przepełnienie stosu: czy wyjątki naprawdę dotyczą wyjątkowych błędów?

„Jaki jest powód istnienia try-else-else?”

Sama klauzula else jest interesująca. Działa, gdy nie ma wyjątku, ale przed klauzulą ​​„w końcu”. To jest jego główny cel.

Bez klauzuli else jedyną opcją uruchomienia dodatkowego kodu przed finalizacją byłaby niezdarna praktyka dodawania kodu do klauzuli try. Jest to nieporadne, ponieważ grozi podniesieniem wyjątków w kodzie, które nie miały być chronione przez try-block.

Przypadek użycia uruchomienia dodatkowego niezabezpieczonego kodu przed finalizacją nie pojawia się zbyt często. Nie spodziewaj się więc, że zobaczysz wiele przykładów w opublikowanym kodzie. To jest dość rzadkie.

Innym przypadkiem użycia klauzuli else jest wykonywanie działań, które muszą wystąpić, gdy nie wystąpi żaden wyjątek i które nie występują, gdy obsługiwane są wyjątki. Na przykład:

recip = float('Inf')
try:
    recip = 1 / f(x)
except ZeroDivisionError:
    logging.info('Infinite result')
else:
    logging.info('Finite result')

Kolejny przykład występuje u niedowiarków:

try:
    tests_run += 1
    run_testcase(case)
except Exception:
    tests_failed += 1
    logging.exception('Failing test case: %r', case)
    print('F', end='')
else:
    logging.info('Successful test case: %r', case)
    print('.', end='')

Wreszcie, najczęstszym zastosowaniem klauzuli else w bloku try jest upiększanie (wyrównanie wyników wyjątkowych i wyników innych niż wyjątkowe na tym samym poziomie wcięcia). Takie użycie jest zawsze opcjonalne i nie jest absolutnie konieczne.

Raymond Hettinger
źródło
28
„Jest to niezdarne, ponieważ grozi podniesieniem wyjątków w kodzie, które nie miały być chronione przez blok try”. To jest najważniejsza nauka tutaj
Felix Dombek
2
Dziękuję za odpowiedź. Dla czytelników szukających przykładów użycia try-wyjątkiem-else, spójrz na metodę copyil
pliku
2
Chodzi o to, że klauzula else jest wykonywana tylko wtedy, gdy klauzula try zakończy się powodzeniem.
Jonathan
172

Jaki jest powód istnienia try-else-else?

tryBlok pozwala na obsługę oczekiwany błąd. exceptBlok powinien złapać tylko wyjątki jesteś przygotowany do obsługi. Jeśli poradzisz sobie z nieoczekiwanym błędem, Twój kod może zrobić coś złego i ukryć błędy.

elseKlauzula będzie wykonać, jeśli nie było żadnych błędów, a poprzez nie wykonanie tego kodu w trybloku można uniknąć łapania nieoczekiwany błąd. Znów złapanie nieoczekiwanego błędu może ukryć błędy.

Przykład

Na przykład:

try:
    try_this(whatever)
except SomeException as the_exception:
    handle(the_exception)
else:
    return something

Pakiet „spróbuj, z wyjątkiem” zawiera dwie opcjonalne klauzule elsei finally. Tak to jest właściwie try-except-else-finally.

elseoceni tylko wtedy, gdy nie ma wyjątku od trybloku. Pozwala nam uprościć bardziej skomplikowany kod poniżej:

no_error = None
try:
    try_this(whatever)
    no_error = True
except SomeException as the_exception:
    handle(the_exception)
if no_error:
    return something

więc jeśli porównamy elsealternatywę (która może powodować błędy), zobaczymy, że zmniejsza ona liczbę wierszy kodu i możemy mieć bardziej czytelną, łatwą w utrzymaniu i mniej wadliwą bazę kodu.

finally

finally wykona się bez względu na wszystko, nawet jeśli inny wiersz jest oceniany za pomocą instrukcji return.

Podział na pseudo-kod

Może to pomóc w rozbiciu tego, w możliwie najmniejszej formie, która pokazuje wszystkie funkcje, z komentarzami. Załóżmy, że pseudo-kod jest poprawny pod względem składniowym (ale nie można go uruchomić, chyba że zdefiniowane są nazwy).

Na przykład:

try:
    try_this(whatever)
except SomeException as the_exception:
    handle_SomeException(the_exception)
    # Handle a instance of SomeException or a subclass of it.
except Exception as the_exception:
    generic_handle(the_exception)
    # Handle any other exception that inherits from Exception
    # - doesn't include GeneratorExit, KeyboardInterrupt, SystemExit
    # Avoid bare `except:`
else: # there was no exception whatsoever
    return something()
    # if no exception, the "something()" gets evaluated,
    # but the return will not be executed due to the return in the
    # finally block below.
finally:
    # this block will execute no matter what, even if no exception,
    # after "something" is eval'd but before that value is returned
    # but even if there is an exception.
    # a return here will hijack the return functionality. e.g.:
    return True # hijacks the return in the else clause above

Prawdą jest, że zamiast tego moglibyśmy umieścić kod w elsebloku w trybloku, gdzie byłby uruchamiany, gdyby nie było wyjątków, ale co, jeśli sam kod wywoła wyjątek, który łapiemy? Pozostawienie go w trybloku ukryłoby ten błąd.

Chcemy zminimalizować wiersze kodu w trybloku, aby uniknąć przechwytywania wyjątków, których się nie spodziewaliśmy, zgodnie z zasadą, że jeśli nasz kod zawiedzie, chcemy, aby zawodził głośno. To najlepsza praktyka .

Rozumiem, że wyjątki nie są błędami

W Pythonie większość wyjątków stanowią błędy.

Możemy wyświetlić hierarchię wyjątków za pomocą pydoc. Na przykład w Python 2:

$ python -m pydoc exceptions

lub Python 3:

$ python -m pydoc builtins

Daje nam hierarchię. Widzimy, że większość rodzajów Exceptionbłędów to błędy, chociaż Python używa niektórych z nich do takich rzeczy jak kończenie forpętli ( StopIteration). Oto hierarchia Pythona 3:

BaseException
    Exception
        ArithmeticError
            FloatingPointError
            OverflowError
            ZeroDivisionError
        AssertionError
        AttributeError
        BufferError
        EOFError
        ImportError
            ModuleNotFoundError
        LookupError
            IndexError
            KeyError
        MemoryError
        NameError
            UnboundLocalError
        OSError
            BlockingIOError
            ChildProcessError
            ConnectionError
                BrokenPipeError
                ConnectionAbortedError
                ConnectionRefusedError
                ConnectionResetError
            FileExistsError
            FileNotFoundError
            InterruptedError
            IsADirectoryError
            NotADirectoryError
            PermissionError
            ProcessLookupError
            TimeoutError
        ReferenceError
        RuntimeError
            NotImplementedError
            RecursionError
        StopAsyncIteration
        StopIteration
        SyntaxError
            IndentationError
                TabError
        SystemError
        TypeError
        ValueError
            UnicodeError
                UnicodeDecodeError
                UnicodeEncodeError
                UnicodeTranslateError
        Warning
            BytesWarning
            DeprecationWarning
            FutureWarning
            ImportWarning
            PendingDeprecationWarning
            ResourceWarning
            RuntimeWarning
            SyntaxWarning
            UnicodeWarning
            UserWarning
    GeneratorExit
    KeyboardInterrupt
    SystemExit

Komentator zapytał:

Powiedzmy, że masz metodę, która wysyła ping do zewnętrznego API i chcesz obsłużyć wyjątek w klasie spoza opakowania API, czy po prostu zwracasz e z metody w ramach klauzuli wyjątku, gdzie e jest obiektem wyjątku?

Nie, nie raisezwrócisz wyjątku, po prostu ponownie go unieś, aby zachować ślad stosu.

try:
    try_this(whatever)
except SomeException as the_exception:
    handle(the_exception)
    raise

Lub, w Python 3, możesz zgłosić nowy wyjątek i zachować ślad za pomocą łączenia wyjątków:

try:
    try_this(whatever)
except SomeException as the_exception:
    handle(the_exception)
    raise DifferentException from the_exception

Rozwijam tutaj swoją odpowiedź .

Aaron Hall
źródło
entuzjastycznie! co zwykle robisz w części uchwytu? powiedzmy, że masz metodę, która wysyła ping do zewnętrznego API i chcesz obsłużyć wyjątek w klasie spoza opakowania API, czy po prostu zwracasz e z metody w ramach klauzuli wyjątku, gdzie e jest obiektem wyjątku?
PirateApp
1
@PirateApp dziękuję! nie, nie zwracaj go, prawdopodobnie powinieneś przebić się z nagim raiselub zrobić łańcuch wyjątków - ale to więcej na temat i omówione tutaj: stackoverflow.com/q/2052390/541136 - prawdopodobnie usunę te komentarze po widziałem, że ich widziałeś.
Aaron Hall
bardzo dziękuję za szczegóły! przechodzi teraz przez post
PirateApp
36

Python nie podziela poglądu, że wyjątków należy używać tylko w wyjątkowych przypadkach, w rzeczywistości idiomem jest „prosić o wybaczenie, a nie pozwolenie” . Oznacza to, że stosowanie wyjątków jako rutynowej części kontroli przepływu jest całkowicie dopuszczalne, a wręcz zalecane.

Zasadniczo jest to dobra rzecz, ponieważ działanie w ten sposób pomaga uniknąć pewnych problemów (jako oczywisty przykład często eliminuje się warunki wyścigu) i sprawia, że ​​kod jest nieco bardziej czytelny.

Wyobraź sobie, że masz sytuację, w której pobierasz dane od użytkowników, które muszą zostać przetworzone, ale masz domyślną, która jest już przetworzona. try: ... except: ... else: ...Konstrukcja sprawia, że bardzo czytelnego kodu:

try:
   raw_value = int(input())
except ValueError:
   value = some_processed_value
else: # no error occured
   value = process_value(raw_value)

Porównaj z tym, jak może działać w innych językach:

raw_value = input()
if valid_number(raw_value):
    value = process_value(int(raw_value))
else:
    value = some_processed_value

Zwróć uwagę na zalety. Nie ma potrzeby sprawdzania, czy wartość jest poprawna i parsowania jej osobno, są one wykonywane raz. Kod również postępuje bardziej logicznie, najpierw główna ścieżka do kodu, a po nim „jeśli to nie działa, zrób to”.

Ten przykład jest oczywiście nieco wymyślony, ale pokazuje, że istnieją przypadki dla tej struktury.

Gareth Latty
źródło
15

Czy dobrą praktyką jest używanie try-wyjątkiem-else w pythonie?

Odpowiedź na to jest zależna od kontekstu. Jeśli to zrobisz:

d = dict()
try:
    item = d['item']
except KeyError:
    item = 'default'

To pokazuje, że nie znasz dobrze Pythona. Ta funkcja jest zawarta w dict.getmetodzie:

item = d.get('item', 'default')

try/ exceptBlok jest dużo bardziej wizualnie bałagan i rozwlekły sposób pisania, co może być skutecznie wykonywany w jednej linii z metodą atomowej. Istnieją inne przypadki, w których jest to prawdą.

Nie oznacza to jednak, że powinniśmy unikać wszelkiej obsługi wyjątków. W niektórych przypadkach preferowane jest unikanie warunków wyścigu. Nie sprawdzaj, czy plik istnieje, po prostu spróbuj go otworzyć i złap odpowiedni błąd IOError. Ze względu na prostotę i czytelność staraj się to kapsułkować lub rozróżnij jako apropos.

Przeczytaj Zen Pythona , rozumiejąc, że istnieją napięte zasady i uważaj na dogmaty, które zbyt mocno opierają się na którymkolwiek z zawartych w nim stwierdzeń.

Aaron Hall
źródło
12

Zobacz następujący przykład, który ilustruje wszystko o try-wyjątkiem-else-wreszcie:

for i in range(3):
    try:
        y = 1 / i
    except ZeroDivisionError:
        print(f"\ti = {i}")
        print("\tError report: ZeroDivisionError")
    else:
        print(f"\ti = {i}")
        print(f"\tNo error report and y equals {y}")
    finally:
        print("Try block is run.")

Wdróż to i przyjdź:

    i = 0
    Error report: ZeroDivisionError
Try block is run.
    i = 1
    No error report and y equals 1.0
Try block is run.
    i = 2
    No error report and y equals 0.5
Try block is run.
Rachunek różniczkowy
źródło
3
To świetny, prosty przykład, który szybko pokazuje klauzulę pełnej próby bez wymagania od kogoś (kto może się spieszyć) przeczytania długiego abstrakcyjnego wyjaśnienia. (Oczywiście, kiedy już się nie spieszą, powinni wrócić i przeczytać pełny streszczenie.)
GlobalSoftwareSociety,
6

Powinieneś być ostrożny z użyciem bloku w końcu, ponieważ nie jest to to samo, co użycie bloku else w próbie, z wyjątkiem. Ostatecznie blok zostanie uruchomiony niezależnie od wyniku próby, z wyjątkiem.

In [10]: dict_ = {"a": 1}

In [11]: try:
   ....:     dict_["b"]
   ....: except KeyError:
   ....:     pass
   ....: finally:
   ....:     print "something"
   ....:     
something

Jak wszyscy zauważyli, użycie bloku else powoduje, że kod jest bardziej czytelny i działa tylko wtedy, gdy wyjątek nie zostanie zgłoszony

In [14]: try:
             dict_["b"]
         except KeyError:
             pass
         else:
             print "something"
   ....:
Greg
źródło
Wiem, że w końcu jest zawsze wykonywane i dlatego można go wykorzystać na naszą korzyść, ustawiając zawsze wartość domyślną, więc w przypadku wyjątku jest on zwracany, jeśli nie chcemy zwracać takiej wartości w przypadku wyjątku, to wystarczy usunąć ostatni blok. A tak przy okazji, przekazując wyjątek, nigdy bym tego nie zrobił :)
Juan Antonio Gomez Moriano
@Juan Antonio Gomez Moriano, mój blok kodowania służy tylko przykładowo. Prawdopodobnie nigdy też nie użyłbym przepustki
Greg
4

Ilekroć to zobaczysz:

try:
    y = 1 / x
except ZeroDivisionError:
    pass
else:
    return y

Lub nawet to:

try:
    return 1 / x
except ZeroDivisionError:
    return None

Zastanów się nad tym:

import contextlib
with contextlib.suppress(ZeroDivisionError):
    return 1 / x
Rajiv Bakulesh Shah
źródło
1
Nie odpowiada na moje pytanie, ponieważ był to po prostu przykład mojego przyjaciela.
Juan Antonio Gomez Moriano
W Pythonie wyjątki nie są błędami. Nie są nawet wyjątkowi. W Pythonie normalne i naturalne jest stosowanie wyjątków do kontroli przepływu. Dowodzi tego włączenie kontekstulib.suppress () do standardowej biblioteki. Zobacz odpowiedź Raymonda Hettingera tutaj: stackoverflow.com/a/16138864/1197429 (Raymond jest głównym współpracownikiem Pythona i autorytetem we wszystkich kwestiach związanych z Pythonem!)
Rajiv Bakulesh Shah
4

Powiedziałbym, że tylko dlatego, że nikt inny nie opublikował tej opinii

unikaj elseklauzul, try/excepts ponieważ są one nieznane większości ludzi

W przeciwieństwie do słów kluczowych try, excepti finallyznaczenie elsezdania nie jest oczywiste; jest mniej czytelny. Ponieważ nie jest często używany, sprawi, że osoby czytające Twój kod będą chciały dokładnie sprawdzić dokumenty, aby upewnić się, że rozumieją, co się dzieje.

(Piszę tę odpowiedź właśnie dlatego, że znalazłem try/except/elsew mojej bazie kodu, co spowodowało chwilę wtf i zmusiło mnie do trochę googlowania).

Gdziekolwiek widzę kod, taki jak przykład OP:

try:
    try_this(whatever)
except SomeException as the_exception:
    handle(the_exception)
else:
    # do some more processing in non-exception case
    return something

Wolę refaktoryzować

try:
    try_this(whatever)
except SomeException as the_exception:
    handle(the_exception)
    return  # <1>
# do some more processing in non-exception case  <2>
return something
  • <1> wyraźny zwrot, wyraźnie pokazuje, że w wyjątkowym przypadku zakończyliśmy pracę

  • <2> jako miły drobny efekt uboczny, kod, który kiedyś był w elsebloku, jest wydzielany z jednego poziomu.

hwjp
źródło
1
Argumentacja diabła: im więcej ludzie go używają, tym bardziej będzie dobrze przyjęty. Tylko do przemyślenia, choć zgadzam się, że czytelność jest ważna. To powiedziawszy, gdy ktoś zrozumie próbę-innego, argumentowałbym, że w wielu przypadkach jest on znacznie bardziej czytelny niż alternatywa.
Bob
2

Oto mój prosty fragment opisujący, w jaki sposób zrozumieć blok try-oprócz-else-w końcu w Pythonie:

def div(a, b):
    try:
        a/b
    except ZeroDivisionError:
        print("Zero Division Error detected")
    else:
        print("No Zero Division Error")
    finally:
        print("Finally the division of %d/%d is done" % (a, b))

Spróbujmy div 1/1:

div(1, 1)
No Zero Division Error
Finally the division of 1/1 is done

Spróbujmy div 1/0

div(1, 0)
Zero Division Error detected
Finally the division of 1/0 is done
zakiakhmad
źródło
1
Myślę, że to nie ilustruje, dlaczego nie możesz po prostu umieścić kodu else w próbie
Mojimi
-4

OP, JESTEŚ PRAWIDŁOWY. Inna po wypróbowaniu / oprócz Pythona jest brzydka . prowadzi do innego obiektu kontroli przepływu, w którym żaden nie jest potrzebny:

try:
    x = blah()
except:
    print "failed at blah()"
else:
    print "just succeeded with blah"

Całkowicie wyraźnym odpowiednikiem jest:

try:
    x = blah()
    print "just succeeded with blah"
except:
    print "failed at blah()"

Jest to o wiele wyraźniejsze niż klauzula else. Inna próba / wyjątek nie jest często zapisywana, więc zastanawianie się, jakie są tego konsekwencje, zajmuje chwilę.

To, że MOŻESZ coś zrobić, nie oznacza, że ​​POWINIENEŚ coś zrobić.

Do języków dodano wiele funkcji, ponieważ ktoś pomyślał, że może się przydać. Problem w tym, że im więcej funkcji, tym mniej jasne i oczywiste są rzeczy, ponieważ ludzie zwykle nie używają tych dzwonków i gwizdków.

Tylko moje 5 centów tutaj. Muszę odejść i wyczyścić dużo kodu napisanego przez programistów college'ów pierwszego roku, którzy myślą, że są inteligentni i chcą pisać kod w niesamowicie ciasny i efektywny sposób, kiedy to tylko robi bałagan spróbować później przeczytać / zmodyfikować. Głosuję za czytelnością każdego dnia i dwa razy w niedziele.

Kevin J. Rice
źródło
15
Masz rację. To całkowicie jasne i równoważne ... chyba że twoje printzdanie się nie powiedzie. Co się stanie, jeśli x = blah()zwróci a str, ale wyciąg z rachunku jest print 'just succeeded with blah. x == %d' % x? Teraz masz TypeErrorgenerację, w której nie jesteś przygotowany do obsługi jednego; sprawdzasz, x = blah()aby znaleźć źródło wyjątku, którego nawet tam nie ma. Zrobiłem to (lub ekwiwalent) więcej niż raz, gdzie elsepowstrzymałoby mnie to od popełnienia tego błędu. Teraz wiem lepiej. :-D
Doug R.
2
... i tak, masz rację. elseKlauzula nie jest całkiem oświadczenie, i dopóki nie jesteś do tego przyzwyczajony, to nie jest intuicyjne. Ale też nie było, finallykiedy po raz pierwszy zacząłem go używać ...
Doug R.
2
Aby powtórzyć Douga R., nie jest to równoważne, ponieważ wyjątki podczas instrukcji w elseklauzuli nie są wychwytywane przez except.
alastair
if ... oprócz ... else jest bardziej czytelny, w przeciwnym razie musisz przeczytać "och, po bloku try i bez wyjątku, przejdź do instrukcji poza blokiem try", więc użycie else: ma tendencję do semantycznego łączenia składni trochę lepsze imo. Ponadto dobrą praktyką jest pozostawienie niewykorzystanych instrukcji poza początkowym blokiem try.
cowbert
1
@DougR. „przeprowadzasz inspekcję, x = blah()aby znaleźć źródło wyjątku”, a tracebackdlaczego miałbyś sprawdzać źródło wyjątku z niewłaściwego miejsca?
nehem