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 None stają 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.