Lepiej „spróbować” czegoś i wyłapać wyjątek lub sprawdzić, czy można najpierw uniknąć wyjątku?

139

Czy powinienem przetestować, czy ifcoś jest prawidłowe, czy po prostu tryto zrobić i wyłapać wyjątek?

  • Czy istnieje solidna dokumentacja mówiąca, że ​​jeden sposób jest preferowany?
  • Czy jest jeden sposób bardziej pytoniczny ?

Na przykład, czy powinienem:

if len(my_list) >= 4:
    x = my_list[3]
else:
    x = 'NO_ABC'

Lub:

try:
    x = my_list[3]
except IndexError:
    x = 'NO_ABC'

Kilka myśli ...
PEP 20 mówi:

Błędy nigdy nie powinny przejść bezgłośnie.
Chyba że wyraźnie uciszono.

Czy użycie tryzamiast an ifpowinno być interpretowane jako cichy błąd? A jeśli tak, to czy wyraźnie go uciszasz, używając go w ten sposób, dzięki czemu jest OK?


Ja nie odnosząc się do sytuacji, w których można zrobić tylko rzeczy 1 drogę; na przykład:

try:
    import foo
except ImportError:
    import baz
chown
źródło

Odpowiedzi:

161

Należy wolisz try/exceptna if/elserazie, że wyniki w

  • przyspieszenia (na przykład poprzez zapobieganie dodatkowym wyszukiwaniom)
  • czystszy kod (mniej linii / łatwiejszy do odczytania)

Często idą ze sobą w parze.


przyspieszenia

W przypadku próby znalezienia elementu na długiej liście poprzez:

try:
    x = my_list[index]
except IndexError:
    x = 'NO_ABC'

try, oprócz jest najlepszą opcją, gdy indexprawdopodobnie znajduje się na liście, a błąd IndexError zwykle nie jest zgłaszany. W ten sposób unikniesz potrzeby dodatkowego wyszukiwania wg if index < len(my_list).

Python zachęca do korzystania z wyjątków, którymi się zajmujesz jest frazą z Dive Into Python . Twój przykład nie tylko obsługuje wyjątek (z wdziękiem), zamiast pozwolić mu przejść po cichu , również wyjątek występuje tylko w wyjątkowym przypadku, gdy indeks nie został znaleziony (stąd słowo wyjątek !).


czystszy kod

Oficjalna dokumentacja Pythona wspomina o EAFP : łatwiej prosić o wybaczenie niż o pozwolenie, a Rob Knight zauważa, że wyłapywanie błędów zamiast ich unikania może skutkować czystszym i łatwiejszym do odczytania kodem. Jego przykład mówi to tak:

Gorzej (LBYL 'patrz przed skokiem') :

#check whether int conversion will raise an error
if not isinstance(s, str) or not s.isdigit():
    return None
elif len(s) > 10:    #too many digits for int conversion
    return None
else:
    return int(s)

Lepiej (EAFP: łatwiej prosić o przebaczenie niż o pozwolenie) :

try:
    return int(s)
except (TypeError, ValueError, OverflowError): #int conversion failed
    return None
Remi
źródło
if index in mylisttesty czy indeks jest elementem mylisty, a nie możliwym indeksem. Wolałbyś if index < len(mylist)zamiast tego.
ychaouche
1
Ma to sens, ale gdzie mogę znaleźć dokumentację, która wyjaśnia, jakie możliwe wyjątki mogą być zgłaszane dla int (). docs.python.org/3/library/functions.html#int Nie mogę tutaj znaleźć tych informacji.
BrutalSimplicity
if/elsetry/catch
Wręcz
20

W tym konkretnym przypadku powinieneś użyć czegoś zupełnie innego:

x = myDict.get("ABC", "NO_ABC")

Ogólnie jednak: jeśli spodziewasz się, że test często kończy się niepowodzeniem, użyj if. Jeśli test jest kosztowny w porównaniu z próbą wykonania operacji i przechwyceniem wyjątku, jeśli się nie powiedzie, użyj try. Jeśli żaden z tych warunków nie ma zastosowania, przejdź do łatwiejszego odczytu.

duskwuff-nieaktywny-
źródło
1
+1 za wyjaśnienie pod przykładem kodu, który jest na miejscu.
ktdrv
4
Myślę, że jest całkiem jasne, że nie o to prosił, a teraz zredagował post, aby był jeszcze bardziej jasny.
agf
9

Używanie tryi exceptbezpośrednio, zamiast wewnątrz ifstrażnik powinien zawsze być wykonane, jeśli istnieje jakakolwiek możliwość wystąpienia sytuacji wyścigu. Na przykład, jeśli chcesz mieć pewność, że katalog istnieje, nie rób tego:

import os, sys
if not os.path.isdir('foo'):
  try:
    os.mkdir('foo')
  except OSError, e
    print e
    sys.exit(1)

Jeśli inny wątek lub proces utworzy katalog między isdira mkdir, zakończysz pracę. Zamiast tego zrób to:

import os, sys, errno
try:
  os.mkdir('foo')
except OSError, e
  if e.errno != errno.EEXIST:
    print e
    sys.exit(1)

To zakończy się tylko wtedy, gdy nie można utworzyć katalogu „foo”.

John Cowan
źródło
7

Jeśli sprawdzenie, czy coś nie powiedzie się, zanim to zrobisz, jest trywialne, prawdopodobnie powinieneś to faworyzować. W końcu tworzenie wyjątków (w tym związanych z nimi śledzenia) zajmuje trochę czasu.

Wyjątki należy stosować do:

  1. rzeczy, które są nieoczekiwane lub ...
  2. rzeczy, w których musisz przeskoczyć o więcej niż jeden poziom logiki (np. gdy a breaknie prowadzi cię wystarczająco daleko) lub ...
  3. rzeczy, w przypadku których nie wiesz dokładnie, co będzie obsługiwać wyjątek z wyprzedzeniem lub ...
  4. rzeczy, w których sprawdzanie z wyprzedzeniem pod kątem awarii jest kosztowne (w porównaniu z samą próbą wykonania operacji)

Zauważ, że często prawdziwa odpowiedź brzmi „nie” - na przykład w pierwszym przykładzie, co naprawdę powinieneś zrobić, to .get()podać wartość domyślną:

x = myDict.get('ABC', 'NO_ABC')
Bursztyn
źródło
z wyjątkiem tego, że if 'ABC' in myDict: x = myDict['ABC']; else: x = 'NO_ABC'jest to faktycznie często szybsze niż używanie get, niestety. Nie mówię, że to najważniejsze kryteria, ale należy być tego świadomym.
agf
@agf: Lepiej pisać jasny i zwięzły kod. Jeśli coś wymaga późniejszej optymalizacji, łatwo jest wrócić i przepisać, ale c2.com/cgi/wiki?PrematureOptimization
Amber
Ja wiem, że; Chodziło mi o to if / elsei try / exceptmogą mieć swoje miejsce nawet wtedy, gdy istnieją alternatywy specyficzne dla danego przypadku, ponieważ mają one różne charakterystyki wydajności.
agf
@agf, czy wiesz, czy metoda get () zostanie ulepszona w przyszłych wersjach, aby była (przynajmniej) tak szybka, jak jawne wyszukiwanie? Nawiasem mówiąc, patrząc dwa razy w górę (jak gdyby 'ABC' w d: d ['ABC']), czy spróbuj: d ['ABC'] oprócz KeyError: ... nie najszybciej?
Remi
2
@Remi Powolną częścią .get()jest wyszukiwanie atrybutów i narzut wywołań funkcji na poziomie Pythona; używanie słów kluczowych we wbudowanych w zasadzie prowadzi bezpośrednio do C. Nie sądzę, aby w najbliższym czasie było to zbyt szybsze. Jeśli chodzi o ifvs. try, metoda read dict.get () zwraca wskaźnik, który zawiera informacje o wydajności. Stosunek trafień do chybień ma znaczenie ( trymoże być szybszy, jeśli klucz prawie zawsze istnieje), podobnie jak rozmiar słownika.
agf
5

Jak wspominają inne posty, zależy to od sytuacji. Istnieje kilka niebezpieczeństw związanych z używaniem try / z wyjątkiem wcześniejszego sprawdzania poprawności danych, szczególnie podczas używania ich w większych projektach.

  • Kod w bloku try może mieć szansę siać spustoszenie, zanim wyjątek zostanie przechwycony - jeśli wcześniej sprawdzisz proaktywnie za pomocą instrukcji if, możesz tego uniknąć.
  • Jeśli kod wywołany w twoim bloku try wywołuje typowy typ wyjątku, taki jak TypeError lub ValueError, możesz nie złapać tego samego wyjątku, którego spodziewałeś się złapać - może to być coś innego, co zgłosi tę samą klasę wyjątku przed lub nawet po uzyskaniu wiersz, w którym można zgłosić wyjątek.

np. załóżmy, że masz:

try:
    x = my_list[index_list[3]]
except IndexError:
    x = 'NO_ABC'

IndexError nie mówi nic o tym, czy wystąpił podczas próby pobrania elementu index_list czy my_list.

Piotr
źródło
4

Czy użycie try zamiast if powinno być interpretowane jako cichy błąd? A jeśli tak, to czy wyraźnie go uciszasz, używając go w ten sposób, dzięki czemu jest OK?

Używanie tryto uznanie, że błąd może minąć, co jest przeciwieństwem cichego przejścia. Za pomocąexcept powoduje, że w ogóle nie przechodzi.

Używanie try: except:jest preferowane w przypadkach, gdyif: else: logika jest bardziej skomplikowana. Proste jest lepsze niż złożone; złożone jest lepsze niż skomplikowane; i łatwiej jest prosić o przebaczenie niż o pozwolenie.

To, o czym ostrzega „błędy nigdy nie przechodzą po cichu”, to przypadek, w którym kod może zgłosić wyjątek, o którym wiesz, i gdzie projekt dopuszcza taką możliwość, ale nie zaprojektowałeś go w sposób radzący sobie z wyjątkiem. Moim zdaniem jawne uciszanie błędu byłoby czymś w rodzaju passplikuexcept bloku, co powinno być wykonywane tylko ze zrozumieniem, że „nic nie robienie” jest w rzeczywistości poprawną obsługą błędu w danej sytuacji. (To jeden z nielicznych przypadków, w których wydaje mi się, że komentarz w dobrze napisanym kodzie jest prawdopodobnie naprawdę potrzebny).

Jednak w twoim konkretnym przykładzie żadne z nich nie jest właściwe:

x = myDict.get('ABC', 'NO_ABC')

Powodem, dla którego wszyscy na to zwracają uwagę - nawet jeśli uznajesz swoje ogólne pragnienie zrozumienia i niezdolność do wymyślenia lepszego przykładu - jest to, że równoważne kroki poboczne faktycznie istnieją w wielu przypadkach, a ich szukanie jest pierwszy krok w rozwiązaniu problemu.

Karl Knechtel
źródło
3

Ilekroć używasz try/exceptdo sterowania przepływem, zadaj sobie pytanie:

  1. Czy łatwo jest zobaczyć, kiedy tryblok się powiedzie, a kiedy zawodzi?
  2. Czy jesteś świadomy wszystkich skutków ubocznych wewnątrz trybloku?
  3. Czy znasz wszystkie przypadki, w których tryblok zgłasza wyjątek?
  4. Jeśli implementacja trybloku ulegnie zmianie, czy przepływ sterowania nadal będzie zachowywał się zgodnie z oczekiwaniami?

Jeśli odpowiedź na jedno lub więcej z tych pytań brzmi „nie”, można prosić o dużo przebaczenia; najprawdopodobniej od przyszłego ja.


Przykład. Niedawno widziałem kod w większym projekcie, który wyglądał tak:

try:
    y = foo(x)
except ProgrammingError:
    y = bar(x)

Rozmawiając z programistą okazało się, że zamierzony przepływ sterowania to:

Jeśli x jest liczbą całkowitą, zrób y = foo (x).

Jeśli x jest listą liczb całkowitych, zrób y = bar (x).

To zadziałało, ponieważ foowykonano zapytanie do bazy danych i zapytanie byłoby pomyślne, gdyby xbyło liczbą całkowitą i wyrzuciło ProgrammingErrorif xbyło listą.

Używanie try/exceptjest tutaj złym wyborem:

  1. Nazwa wyjątku ProgrammingErrornie zdradza rzeczywistego problemu ( xnie jest to liczba całkowita), co utrudnia dostrzeżenie, co się dzieje.
  2. ProgrammingErrorJest podnoszona podczas połączenia bazy danych, która w czasie odpady. Sytuacja stałaby się naprawdę okropna, gdyby się okazało, że foozapisuje coś do bazy danych, zanim zgłosi wyjątek lub zmieni stan innego systemu.
  3. Nie jest jasne, czy ProgrammingErrorjest zgłaszany tylko wtedy, gdy xjest listą liczb całkowitych. Załóżmy na przykład, że w foozapytaniu do bazy danych jest literówka . Może to również spowodować wzrost ProgrammingError. Konsekwencją jest to, że bar(x)jest teraz wywoływana również kiedy xjest liczbą całkowitą. Może to wywołać tajemnicze wyjątki lub spowodować nieprzewidziane skutki.
  4. try/exceptBlok dodaje wymóg wszystkich implementacjach przyszłości foo. Za każdym razem, gdy się zmieniamy foo, musimy teraz pomyśleć o tym, jak obsługuje on listy i upewnić się, że zgłasza błąd, ProgrammingErrora nie, powiedzmy, AttributeErrorżaden błąd.
Elias Strehle
źródło
0

Dla ogólnego znaczenia możesz rozważyć przeczytanie idiomów i anty-idiomów w Pythonie: wyjątki .

W swoim konkretnym przypadku, jak powiedzieli inni, powinieneś użyć dict.get():

get (klucz [, domyślny])

Zwróć wartość klucza, jeśli klucz znajduje się w słowniku, w przeciwnym razie wartość domyślna. Jeśli nie podano wartości default, przyjmuje wartość domyślną None, więc ta metoda nigdy nie zgłasza błędu KeyError.

etuardu
źródło
Nie sądzę, aby link w ogóle obejmował tę sytuację. Jego przykłady dotyczą obsługi rzeczy, które reprezentują rzeczywiste błędy , a nie tego, czy używać obsługi wyjątków w oczekiwanych sytuacjach.
agf
Pierwszy link jest nieaktualny.
Timofei Bondarev