2.26. Zgłaszanie błędów¶
Funkcja może zasygnalizować problem swojemu wywołującemu, zgłaszając wyjątek. Słowem kluczowym jest raise:
def square_root(x):
if x < 0:
raise ValueError("square_root expects a non-negative number")
return x ** 0.5
Wywołanie square_root(-1) zatrzymuje się na wierszu raise, wyskakuje z funkcji i szuka pasującego except u wywołującego. Jeśli żaden wywołujący go nie przechwyci, skrypt kończy się komunikatem śledzenia (traceback).
2.26.1. Dlaczego zgłaszać wyjątek zamiast zwracać wartość-wartownik¶
Dwa sposoby zgłoszenia „błędnego wejścia”:
# signal with a sentinel
def square_root_or_none(x):
if x < 0:
return None
return x ** 0.5
# raise an exception
def square_root(x):
if x < 0:
raise ValueError("...")
return x ** 0.5
Postać z wyjątkiem jest zwykle lepsza:
Wywołujący musi celowo obsłużyć przypadek błędu – albo za pomocą
try, albo pozwalając wyjątkowi się propagować. O wartownikach łatwo zapomnieć i łatwo pomylić je ze zwykłym wynikiem.Komunikat o błędzie podróżuje wraz z wyjątkiem; podejście z wartownikiem musi dołączyć informację diagnostyczną gdzie indziej.
Domyślnym zachowaniem przy nieobsłużonym wyjątku jest głośna awaria z komunikatem śledzenia wskazującym na problematyczne wywołanie. Ciche zwroty
Nonestają się później subtelnymi błędami.
Sięgaj po wartowniki tylko wtedy, gdy „nie znaleziono” jest rutynowym, niewyjątkowym wynikiem – dict.get() zwraca None przy brakującym kluczu właśnie dlatego, że oczekuje się, iż wyszukiwania czasem nie trafiają.
2.26.2. Niestandardowe klasy wyjątków¶
Aby zgłosić problem, który wywołujący może chcieć odróżnić od błędów wbudowanych, zdefiniuj podklasę Exception:
class ConfigError(Exception):
pass
def load_config(path):
try:
f = open(path)
except OSError as e:
raise ConfigError("missing config file: " + path)
try:
load_config("settings.json")
except ConfigError as e:
print("startup failed:", e)
Puste ciało class jest w porządku – liczy się sama nazwa, ponieważ wywołujący przechwytują po klasie. Pogrupuj powiązane błędy pod wspólną bazą, jeśli wywołujący może chcieć przechwycić całą rodzinę w jednym bloku.
2.26.2.1. Ponowne zgłaszanie¶
Goły raise wewnątrz bloku except ponownie zgłasza bieżący wyjątek, tak że propaguje się on do następnej procedury obsługi:
try:
do_work()
except Exception as e:
log(e)
raise # let it keep going
To właściwy kształt, gdy funkcja chce zaobserwować błąd (zalogować go, policzyć, cofnąć częściową zmianę), nie obsługując go faktycznie.
2.26.3. Kiedy przechwytywać, a kiedy propagować¶
Przydatna reguła kciuka:
Przechwytuj wyjątek na poziomie, który potrafi sensownie się z niego podnieść – podstawić wartość domyślną, ponowić próbę, pominąć błędne wejście.
Pozwól mu się propagować, gdy nie ma nic użytecznego do zrobienia poza awarią lub gdy to warstwa powyżej wie, jak się podnieść.
Funkcja w środku stosu wywołań, która połyka każdy błąd i zwraca cicho, czyni awarie niemożliwymi do prześledzenia. Lepiej pozwól wyjątkom podróżować, dopóki nie dotrą do kodu, który naprawdę ma plan.