Ogólny konsensus „nie używaj wyjątków!” przeważnie pochodzi z innych języków i nawet tam są czasem nieaktualne.
C ++, wyrzucanie wyjątek jest bardzo kosztowne, ze względu na „stos odwijania”. Każda deklaracja zmiennej lokalnej jest jak with
instrukcja w Pythonie, a obiekt w tej zmiennej może uruchamiać destruktory. Te destruktory są wykonywane po zgłoszeniu wyjątku, ale także po powrocie z funkcji. Ten „idiom RAII” jest integralną funkcją językową i jest bardzo ważny do pisania solidnego, poprawnego kodu - więc RAII w porównaniu z tanimi wyjątkami było kompromisem, który C ++ zdecydował się na RAII.
We wczesnym C ++ wiele kodu nie zostało napisane w sposób bezpieczny dla wyjątków: chyba że faktycznie używasz RAII, łatwo jest wyciec pamięć i inne zasoby. Zgłaszanie wyjątków spowodowałoby, że ten kod byłby niepoprawny. Nie jest to już uzasadnione, ponieważ nawet standardowa biblioteka C ++ używa wyjątków: nie możesz udawać, że wyjątki nie istnieją. Jednak wyjątki nadal stanowią problem przy łączeniu kodu C z C ++.
W Javie każdy wyjątek ma powiązany ślad stosu. Śledzenie stosu jest bardzo cenne podczas błędów debugowania, ale jest marnowane na wysiłek, gdy wyjątek nigdy nie jest drukowany, np. Ponieważ został użyty tylko do kontroli przepływu.
Tak więc w tych językach wyjątki są „zbyt drogie”, aby można je było zastosować jako kontrolę. W Pythonie jest to mniejszy problem, a wyjątki są znacznie tańsze. Ponadto w języku Python występuje już pewien narzut, który sprawia, że koszt wyjątków jest niezauważalny w porównaniu z innymi konstrukcjami przepływu kontroli: np. Sprawdzenie, czy istnieje wpis dict z jawnym testem członkostwa, if key in the_dict: ...
jest zasadniczo tak samo szybkie, jak samo uzyskanie dostępu do wpisu the_dict[key]; ...
i sprawdzenie, czy użytkownik dostać KeyError. Niektóre integralne funkcje językowe (np. Generatory) zostały zaprojektowane pod kątem wyjątków.
Tak więc, chociaż nie ma technicznego powodu, aby szczególnie unikać wyjątków w Pythonie, wciąż pozostaje pytanie, czy powinieneś ich używać zamiast zwracać wartości. Problemy na poziomie projektowania z wyjątkami to:
wcale nie są oczywiste. Nie możesz łatwo spojrzeć na funkcję i zobaczyć, jakie wyjątki może ona generować, więc nie zawsze wiesz, co złapać. Zwracana wartość jest zwykle lepiej zdefiniowana.
wyjątki to nielokalny przepływ sterowania, który komplikuje kod. Po zgłoszeniu wyjątku nie wiadomo, gdzie wznowiony zostanie przepływ sterowania. W przypadku błędów, których nie można natychmiast usunąć, jest to prawdopodobnie dobry pomysł, gdy powiadamiając osobę dzwoniącą o stanie, jest to całkowicie niepotrzebne.
Kultura Pythona jest na ogół pochylona za wyjątkami, ale łatwo jest przesadzić. Wyobraź sobie list_contains(the_list, item)
funkcję, która sprawdza, czy lista zawiera element równy temu elementowi. Jeśli wynik jest przekazywany za pośrednictwem wyjątków, jest to absolutnie irytujące, ponieważ musimy to tak nazwać:
try:
list_contains(invited_guests, person_at_door)
except Found:
print("Oh, hello {}!".format(person_at_door))
except NotFound:
print("Who are you?")
Zwrócenie bool byłoby znacznie bardziej przejrzyste:
if list_contains(invited_guests, person_at_door):
print("Oh, hello {}!".format(person_at_door))
else:
print("Who are you?")
Jeśli funkcja ma już zwracać wartość, wówczas zwracanie specjalnej wartości dla warunków specjalnych jest raczej podatne na błędy, ponieważ ludzie zapomną sprawdzić tę wartość (prawdopodobnie jest to przyczyną 1/3 problemów w C). Wyjątek jest zwykle bardziej poprawny.
Dobrym przykładem jest pos = find_string(haystack, needle)
funkcja, która wyszukuje pierwsze wystąpienie needle
ciągu w ciągu `stóg siana i zwraca pozycję początkową. Ale co jeśli sznurek stogu siana nie zawiera sznurka igły?
Rozwiązaniem C i naśladowanym przez Python jest zwrócenie specjalnej wartości. W C jest to wskaźnik zerowy, w Pythonie to jest -1
. Doprowadzi to do zaskakujących wyników, gdy pozycja jest używana jako indeks łańcuchowy bez sprawdzania, zwłaszcza tak jak -1
prawidłowy indeks w Pythonie. W C twój wskaźnik NULL da ci przynajmniej awarię.
W PHP zwracana jest specjalna wartość innego typu: wartość logiczna FALSE
zamiast liczby całkowitej. Jak się okazuje, nie jest to wcale lepsze ze względu na niejawne reguły konwersji języka (należy jednak pamiętać, że w Pythonie również logiczne mogą być używane jako int!). Funkcje, które nie zwracają spójnego typu, są ogólnie uważane za bardzo mylące.
Bardziej niezawodnym wariantem byłoby rzucenie wyjątku, gdy nie można znaleźć łańcucha, co zapewnia, że podczas normalnego przepływu sterowania niemożliwe jest przypadkowe użycie specjalnej wartości zamiast zwykłej wartości:
try:
pos = find_string(haystack, needle)
do_something_with(pos)
except NotFound:
...
Alternatywnie, zawsze można zwrócić typ, który nie może być użyty bezpośrednio, ale musi być najpierw rozpakowany, np. Krotka wynikowa bool, gdzie wartość logiczna wskazuje, czy wystąpił wyjątek lub czy wynik jest użyteczny. Następnie:
pos, ok = find_string(haystack, needle)
if not ok:
...
do_something_with(pos)
To zmusza cię do natychmiastowego rozwiązywania problemów, ale szybko się denerwuje. Zapobiega również łatwemu łączeniu funkcji. Każde wywołanie funkcji wymaga teraz trzech wierszy kodu. Golang to język, który uważa, że ta uciążliwość jest warta bezpieczeństwa.
Podsumowując, wyjątki nie są całkowicie pozbawione problemów i mogą być ostatecznie nadużywane, zwłaszcza gdy zastępują „normalną” wartość zwracaną. Ale kiedy są używane do sygnalizowania specjalnych warunków (niekoniecznie tylko błędów), wyjątki mogą pomóc w opracowaniu interfejsów API, które są czyste, intuicyjne, łatwe w użyciu i trudne do niewłaściwego użycia.
collections.defaultdict
lubmy_dict.get(key, default)
kod jest znacznie wyraźniejszy niżtry: my_dict[key] except: return default
NIE! - nie ogólnie - wyjątki nie są uważane za dobrą praktykę kontroli przepływu, z wyjątkiem pojedynczej klasy kodu. Jedynym miejscem, w którym wyjątki są uważane za rozsądny, a nawet lepszy sposób sygnalizowania stanu, są operacje generatora lub iteratora. Operacje te mogą zwrócić dowolną możliwą wartość jako prawidłowy wynik, dlatego potrzebny jest mechanizm sygnalizujący zakończenie.
Zastanów się nad odczytaniem pliku binarnego strumienia jeden bajt naraz - absolutnie każda wartość jest potencjalnie poprawnym wynikiem, ale nadal musimy sygnalizować koniec pliku. Mamy więc wybór, zwracamy dwie wartości (wartość bajtu i prawidłową flagę) za każdym razem lub zgłaszamy wyjątek, gdy nie ma już nic do zrobienia. W dwóch przypadkach konsumujący kod może wyglądać następująco:
alternatywnie:
Ale tak się stało, odkąd PEP 343 został zaimplementowany i ponownie przeniesiony, wszystkie zostały starannie zamknięte w
with
instrukcji. Powyższe staje się bardzo pytoniczne:W python3 stało się to:
Gorąco zachęcam do przeczytania PEP 343, który podaje tło, uzasadnienie, przykłady itp.
Zwykle używa się wyjątku, aby zasygnalizować koniec przetwarzania, gdy używa się funkcji generatora do sygnalizowania zakończenia.
Chciałbym dodać, że poszukiwacz przykładem jest prawie na pewno do tyłu, takie funkcje powinny być generatory, zwracając pierwszy mecz na pierwszej rozmowy, rozmowy wówczas podstawniki powracający następny mecz, a podnoszenie się
NotFound
wyjątek, gdy nie ma więcej meczów.źródło
if
byłby lepszy. Jeśli chodzi o debugowanie i testowanie, a nawet wnioskowanie o zachowaniu się kodów, lepiej nie opierać się na wyjątkach - powinny to być wyjątki. W kodzie produkcyjnym widziałem obliczenia, które zwróciły się za pomocą rzutu / złapania, a nie zwykłego zwrotu, co oznaczało, że gdy obliczenie uderzyło w błąd przez zero, zwrócił losową wartość, a nie zawiesił się, gdy wystąpił błąd, co spowodowało kilka godzin debugowanie.