Jak słusznie zgadłeś, istnieją dwie strony: Wyłapanie dowolnego błędu przez określenie typu wyjątku po except
i przekazanie go bez podejmowania żadnych działań.
Moje wyjaśnienie jest „trochę” dłuższe - więc tl; dr dzieli się na następujące:
- Nie wychwytuj żadnych błędów . Zawsze określaj wyjątki, z których jesteś przygotowany, aby je odzyskać i tylko je wychwyć.
- Staraj się unikać przechodzenia poza blokami . O ile nie jest to wyraźnie pożądane, zwykle nie jest to dobry znak.
Ale przejdźmy do szczegółów:
Nie wychwytuj żadnych błędów
Korzystając z try
bloku, zwykle robisz to, ponieważ wiesz, że istnieje szansa na wygenerowanie wyjątku. Jako taki, masz już przybliżone pojęcie o tym, co może się złamać i jaki wyjątek może zostać zgłoszony. W takich przypadkach wychwytujesz wyjątek, ponieważ możesz go pozytywnie odzyskać . Oznacza to, że jesteś przygotowany na wyjątek i masz alternatywny plan, którego będziesz przestrzegać w przypadku tego wyjątku.
Na przykład, gdy poprosisz użytkownika o wprowadzenie liczby, możesz przekonwertować dane wejściowe za pomocą, int()
który może podnieść liczbę ValueError
. Możesz to łatwo odzyskać, po prostu prosząc użytkownika o ponowną próbę, więc złapanie ValueError
i ponowne monitowanie użytkownika byłoby właściwym planem. Innym przykładem może być odczytanie pewnej konfiguracji z pliku, a plik ten nie istnieje. Ponieważ jest to plik konfiguracyjny, możesz mieć domyślną konfigurację zastępczą, więc plik nie jest koniecznie potrzebny. FileNotFoundError
Dobrym pomysłem byłoby więc złapanie i po prostu zastosowanie domyślnej konfiguracji. Teraz w obu tych przypadkach mamy bardzo specyficzny wyjątek, którego się spodziewamy, i mamy równie konkretny plan, aby go naprawić. W związku z tym w każdym przypadku wyraźnie tylko except
to pewne wyjątek.
Gdybyśmy jednak złapali wszystko , to - oprócz tych wyjątków, z których jesteśmy gotowi wyzdrowieć - istnieje również szansa, że otrzymamy wyjątki, których się nie spodziewaliśmy i z których w rzeczywistości nie możemy wyzdrowieć; lub nie powinien wyzdrowieć.
Weźmy przykład pliku konfiguracyjnego z góry. W przypadku brakującego pliku zastosowaliśmy naszą domyślną konfigurację i w późniejszym czasie możemy zdecydować o automatycznym zapisaniu konfiguracji (więc następnym razem plik będzie istniał). Teraz wyobraź sobie, że dostaniemy IsADirectoryError
alboPermissionError
zamiast. W takich przypadkach prawdopodobnie nie chcemy kontynuować; nadal możemy zastosować naszą domyślną konfigurację, ale później nie będziemy mogli zapisać pliku. I prawdopodobne jest, że użytkownik miał również konfigurację niestandardową, więc użycie wartości domyślnych prawdopodobnie nie jest pożądane. Chcielibyśmy więc natychmiast poinformować o tym użytkownika i prawdopodobnie przerwać również wykonywanie programu. Ale nie chcemy tego robić gdzieś głęboko w jakiejś małej części kodu; jest to coś ważnego na poziomie aplikacji, więc powinno się z tym obchodzić na górze - pozwól więc, by wyjątek wybuchł.
Kolejny prosty przykład jest również wspomniany w dokumencie idiomów Python 2 . W kodzie znajduje się prosta literówka, która powoduje jej uszkodzenie. Ponieważ łapiemy każdy wyjątek, łapiemy również NameError
s i SyntaxError
s . Oba są błędami, które przytrafiają się nam wszystkim podczas programowania; i oba są błędami, których absolutnie nie chcemy uwzględniać przy wysyłaniu kodu. Ale ponieważ złapaliśmy je również, nie będziemy nawet wiedzieć, że tam się pojawiły i nie stracimy żadnej pomocy, aby poprawnie debugować.
Ale są też bardziej niebezpieczne wyjątki, na które nie jesteśmy przygotowani. Na przykład SystemError jest zwykle czymś, co zdarza się rzadko i na co tak naprawdę nie możemy zaplanować; oznacza to, że dzieje się coś bardziej skomplikowanego, coś, co prawdopodobnie uniemożliwia nam kontynuowanie bieżącego zadania.
W każdym razie jest bardzo mało prawdopodobne, że jesteś przygotowany na wszystko w małej części kodu, więc tak naprawdę powinieneś wychwycić tylko te wyjątki, na które jesteś przygotowany. Niektórzy sugerują, przynajmniej połowu Exception
, ponieważ nie będzie obejmować takie rzeczy jak SystemExit
i KeyboardInterrupt
który według projektu mają zakończyć aplikację, ale jestem zdania, że jest to nadal zbyt niespecyficzne. Jest tylko jedno miejsce, w którym osobiście akceptuję łapanie Exception
lub tylko dowolnewyjątek, który znajduje się w jednym globalnym module obsługi wyjątków na poziomie aplikacji, którego jedynym celem jest zarejestrowanie każdego wyjątku, na który nie byliśmy przygotowani. W ten sposób możemy zachować tyle informacji o nieoczekiwanych wyjątkach, które możemy następnie wykorzystać do rozszerzenia naszego kodu, aby jawnie obsługiwać te wyjątki (jeśli możemy je odzyskać) lub - w przypadku błędu - utworzyć przypadki testowe, aby upewnić się to się więcej nie powtórzy. Ale oczywiście działa to tylko wtedy, gdy wychwycimy tylko te wyjątki, których się już spodziewaliśmy, więc te, których się nie spodziewaliśmy, naturalnie wybuchną.
Staraj się unikać wchodzenia, z wyjątkiem bloków
Gdy wyraźnie wychwytuje się niewielki wybór konkretnych wyjątków, istnieje wiele sytuacji, w których wszystko będzie dobrze, po prostu nic nie robiąc. W takich przypadkach samo posiadanie except SomeSpecificException: pass
jest w porządku. Jednak przez większość czasu tak nie jest, ponieważ prawdopodobnie potrzebujemy trochę kodu związanego z procesem odzyskiwania (jak wspomniano powyżej). Może to być na przykład coś, co ponownie próbuje wykonać akcję lub zamiast tego ustawić wartość domyślną.
Jeśli tak nie jest, na przykład dlatego, że nasz kod jest już tak skonstruowany, aby powtarzał się, dopóki się nie powiedzie, to wystarczy przekazać. Biorąc nasz przykład z góry, możemy poprosić użytkownika o wprowadzenie numeru. Ponieważ wiemy, że użytkownicy nie chcą robić tego, o co ich proszymy, możemy po prostu umieścić to w pętli, aby wyglądało to tak:
def askForNumber ():
while True:
try:
return int(input('Please enter a number: '))
except ValueError:
pass
Ponieważ próbujemy, dopóki nie zostanie zgłoszony żaden wyjątek, nie musimy robić nic specjalnego w bloku oprócz, więc jest w porządku. Ale oczywiście można argumentować, że przynajmniej chcemy pokazać użytkownikowi komunikat o błędzie, aby powiedzieć mu, dlaczego musi powtórzyć dane wejściowe.
Jednak w wielu innych przypadkach samo przekazanie znaku except
jest znakiem, że tak naprawdę nie byliśmy przygotowani na wyjątek, który łapiemy. O ile te wyjątki nie są proste (jak ValueError
lub TypeError
), a powód, dla którego możemy przejść, jest oczywisty, staraj się unikać po prostu przejścia. Jeśli naprawdę nie ma nic do zrobienia (i jesteś absolutnie pewien), rozważ dodanie komentarza, dlaczego tak jest; w przeciwnym razie rozwiń blok oprócz, aby faktycznie zawierał kod odzyskiwania.
except: pass
Najgorszym sprawcą jest jednak połączenie obu. Oznacza to, że chętnie wychwytujemy każdy błąd, chociaż absolutnie nie jesteśmy na to przygotowani i nie robimy z tym nic. Ci przynajmniej chcą zalogować błąd i prawdopodobnie również przebić go jeszcze zakończyć działanie aplikacji (jest to mało prawdopodobne, można kontynuować jak normalne po MemoryError). Samo przekazanie nie tylko utrzyma działanie aplikacji (oczywiście w zależności od tego, gdzie złapiesz), ale także wyrzuci wszystkie informacje, uniemożliwiając wykrycie błędu - co jest szczególnie prawdziwe, jeśli to nie ty go odkrywasz.
Więc sedno jest następujące: Złap tylko wyjątki, których naprawdę oczekujesz i jesteś przygotowany na powrót do zdrowia; wszystkie inne to albo błędy, które powinieneś naprawić, albo coś, na co i tak nie jesteś przygotowany. Przekazywanie określonych wyjątków jest w porządku, jeśli naprawdę nie musisz nic z nimi robić. We wszystkich innych przypadkach jest to tylko znak domniemania i lenistwa. I na pewno chcesz to naprawić.
logging
modułu na poziomie DEBUG, aby uniknąć przesyłania strumieniowego w produkcji, ale zachowaj je w fazie rozwoju.