Czy istnieją wyjątki od najlepszych praktyk kontroli przepływu w Pythonie?


15

Czytam „Learning Python” i natrafiłem na następujące:

Zdefiniowane przez użytkownika wyjątki mogą również sygnalizować warunki nieerrorowe. Na przykład, procedura wyszukiwania może zostać zakodowana, aby zgłosić wyjątek, gdy zostanie znalezione dopasowanie, zamiast zwracać flagę statusu do interpretacji przez osobę dzwoniącą. Poniżej program obsługi wyjątków try / wyjątkiem / else wykonuje działanie testera wartości zwracanej if / else:

class Found(Exception): pass

def searcher():
    if ...success...:
        raise Found()            # Raise exceptions instead of returning flags
    else:
        return

Ponieważ Python jest dynamicznie typowany i polimorficzny w rdzeniu, wyjątki, a nie wartości zwrotne wartowników, są ogólnie preferowanym sposobem sygnalizowania takich warunków.

Widziałem takie rzeczy omawiane wiele razy na różnych forach i odniesienia do Pythona używającego StopIteration do zakończenia pętli, ale nie mogę znaleźć wiele w oficjalnych przewodnikach po stylu (PEP 8 ma jedno bezpośrednie odniesienie do wyjątków dotyczących kontroli przepływu) lub oświadczenia deweloperów. Czy jest coś oficjalnego, co stanowi, że jest to najlepsza praktyka dla Pythona?

To ( Czy wyjątki jako kontrola są uważane za poważny antypattern? Jeśli tak, dlaczego? ), Również kilku komentujących twierdzi, że ten styl jest Pythonic. Na czym to opiera się?

TIA

Odpowiedzi:


26

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 withinstrukcja 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 needlecią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 -1prawidł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 FALSEzamiast 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.


1
Dziękuję za szczegółową odpowiedź. Pochodzę z innych języków, takich jak Java, gdzie „wyjątki dotyczą tylko wyjątkowych warunków”, więc jest to dla mnie nowość. Czy są jacyś rdzeniowi twórcy języka Python, którzy twierdzili to tak, jak w przypadku innych wytycznych Python, takich jak EAFP, EIBTI itp. (Na liście mailingowej, blogu itp.) Chciałbym móc zacytować coś oficjalnego, jeśli pyta inny deweloper / szef. Dzięki!
JB

Przeciążając metodę fillInStackTrace niestandardowej klasy wyjątków, aby po prostu natychmiast powróciła, możesz sprawić, że jej podniesienie jest bardzo tanie, ponieważ kosztowne jest chodzenie po stosach i tam właśnie się to odbywa.
John Cowan

Twoja pierwsza kula we wstępie była na początku myląca. Być może można go zmienić na coś w rodzaju „wyjątek kodu stosowania w nowoczesnych C ++ jest zero kosztów, ale rzuca wyjątek jest drogie”? (dla lurkers: stackoverflow.com/questions/13835817/... ) [edytuj: edytuj proponowane]
fresnel

Zauważ, że w przypadku operacji wyszukiwania słownikowego za pomocą a collections.defaultdictlub my_dict.get(key, default)kod jest znacznie wyraźniejszy niżtry: my_dict[key] except: return default
Steve Barnes

11

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:

# Using validity flag
valid, val = readbyte(source)
while valid:
    processbyte(val)
    valid, val = readbyte(source)
tidy_up()

alternatywnie:

# With exceptions
try:
   val = readbyte(source)
   processbyte(val) # Note if a problem occurs here it will also raise an exception
except Exception: # Use a specific exception here!
   tidy_up()

Ale tak się stało, odkąd PEP 343 został zaimplementowany i ponownie przeniesiony, wszystkie zostały starannie zamknięte w withinstrukcji. Powyższe staje się bardzo pytoniczne:

with open(source) as input:
    for val in input.readbyte(): # This line will raise a StopIteration exception an call input.__exit__()
        processbyte(val) # Not called if there is nothing read

W python3 stało się to:

for val in open(source, 'rb').read():
     processbyte(val)

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ę NotFoundwyjątek, gdy nie ma więcej meczów.


Mówisz: „NIE! - nie ogólnie - wyjątki nie są uważane za dobrą praktykę kontroli przepływu, z wyjątkiem pojedynczej klasy kodu”. Jakie jest uzasadnienie tego zdecydowanego stwierdzenia? Ta odpowiedź wydaje się koncentrować na wąskim przypadku iteracji. Istnieje wiele innych przypadków, w których ludzie mogą pomyśleć o zastosowaniu wyjątków. Na czym polega problem w tych innych przypadkach?
Daniel Waltrip

@DanielWaltrip Widzę o wiele za dużo kodu w różnych językach programowania, w których stosowane są wyjątki, często o wiele za szerokie, gdy prosty ifbył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.
Steve Barnes

Hej @SteveBarnes, przykład dzielenia przez zero na błąd łapania brzmi jak kiepskie programowanie (ze zbyt ogólnym chwytem, ​​który nie podnosi ponownie wyjątku).
wTyeRogers

Twoja odpowiedź sprowadza się do: A) „NIE!” B) istnieją ważne przykłady, w których przepływ sterowania za pośrednictwem wyjątków działa lepiej niż alternatywy C) korekta kodu pytania w celu użycia generatorów (które używają wyjątków jako przepływu sterowania i robią to dobrze). Chociaż twoja odpowiedź jest ogólnie informacyjna, wydaje się, że nie ma w niej żadnych argumentów na poparcie punktu A, który, będąc pogrubionym i pozornie podstawowym stwierdzeniem, oczekiwałbym poparcia. Gdyby było obsługiwane, nie głosowałbym za odpowiedzią. Niestety ...
wTyeRogers
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.