Kiedy i jak mam korzystać z wyjątków?

20

Ustawienie

Często mam problemy z określeniem, kiedy i jak korzystać z wyjątków. Rozważmy prosty przykład: załóżmy, że przeglądam stronę internetową, powiedz „ http://www.abevigoda.com/ ”, aby ustalić, czy Abe Vigoda nadal żyje. Aby to zrobić, wystarczy pobrać stronę i poszukać czasów, w których pojawia się zwrot „Abe Vigoda”. Zwracamy pierwszy występ, ponieważ obejmuje to status Abe. Koncepcyjnie będzie to wyglądać tak:

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

Gdzie parse_abe_status(s)przyjmuje ciąg formy „Abe Vigoda jest czymś ” i zwraca część „ coś ”.

Zanim przekonasz się, że istnieją znacznie lepsze i bardziej niezawodne sposoby skrobania tej strony w celu uzyskania statusu Abe, pamiętaj, że jest to prosty i przemyślany przykład użyty do podkreślenia typowej sytuacji, w której się znajduję.

Gdzie ten kod może napotykać problemy? Wśród innych błędów niektóre „oczekiwane” to:

  • download_pagemoże nie być w stanie pobrać strony i zgłasza IOError.
  • Adres URL może nie wskazywać właściwej strony lub strona jest niepoprawnie pobrana, więc nie ma żadnych trafień. hitsjest więc pusta lista.
  • Strona internetowa została zmieniona, co prawdopodobnie czyni nasze założenia dotyczące strony błędnymi. Może oczekujemy 4 wzmianek o Abe Vigodzie, ale teraz znajdujemy 5.
  • Z niektórych powodów hits[0]może nie być ciągiem w formie „Abe Vigoda jest czymś ”, więc nie można go poprawnie przeanalizować.

Pierwszy przypadek nie jest dla mnie problemem: an IOErrorjest rzucany i może być obsłużony przez program wywołujący moją funkcję. Zastanówmy się więc nad innymi przypadkami i jak sobie z nimi poradzić. Ale najpierw załóżmy, że wdrażamy parse_abe_statusw najgłupszy możliwy sposób:

def parse_abe_status(s):
    return s[13:]

Mianowicie nie sprawdza błędów. Teraz przejdź do opcji:

Opcja 1: powrót None

Mogę powiedzieć dzwoniącemu, że coś poszło nie tak, zwracając None:

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    if not hits:
        return None

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

Jeśli dzwoniący otrzymuje Noneod mojej funkcji, powinien założyć, że nie było wzmianek o Abe Vigodzie, a więc coś poszło nie tak. Ale to dość niejasne, prawda? I to nie pomaga w przypadku, gdy hits[0]nie jest tak, jak nam się wydawało.

Z drugiej strony możemy wprowadzić pewne wyjątki:

Opcja 2: Korzystanie z wyjątków

Jeśli hitsjest pusty, IndexErrorzostanie rzucony podczas próby hits[0]. Ale nie należy oczekiwać, że osoba dzwoniąca poradzi sobie z IndexErrorrzuconą przez moją funkcję, ponieważ nie ma pojęcia, skąd ona IndexErrorpochodzi; mogło to zostać zrzucone find_all_mentions, o ile on wie. Dlatego stworzymy niestandardową klasę wyjątków, aby obsłużyć to:

class NotFoundError(Exception):
    """Throw this when something can't be found on a page."""

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    try:
        hits[0]
    except IndexError:
        raise NotFoundError("No mentions found.")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

Co się stanie, jeśli strona ulegnie zmianie i pojawi się nieoczekiwana liczba wyświetleń? Nie jest to katastrofalne, ponieważ kod może nadal działać, ale osoba dzwoniąca może chcieć być bardzo ostrożna lub może zarejestrować ostrzeżenie. Więc rzucę ostrzeżenie:

class NotFoundError(Exception):
    """Throw this when something can't be found on a page."""

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    try:
        hits[0]
    except IndexError:
        raise NotFoundError("No mentions found.")

    # say we expect four hits...
    if len(hits) != 4:
        raise Warning("An unexpected number of hits.")
        logger.warning("An unexpected number of hits.")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

Wreszcie możemy odkryć, że statusnie jest ani żywy, ani martwy. Być może z jakiegoś dziwnego powodu dziś tak się stało comatose. Więc nie chcę wracać False, ponieważ to sugeruje, że Abe nie żyje. Co mam tu zrobić? Prawdopodobnie rzuć wyjątek. Ale jaki? Czy powinienem utworzyć niestandardową klasę wyjątków?

class NotFoundError(Exception):
    """Throw this when something can't be found on a page."""

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    try:
        hits[0]
    except IndexError:
        raise NotFoundError("No mentions found.")

    # say we expect four hits...
    if len(hits) != 4:
        raise Warning("An unexpected number of hits.")
        logger.warning("An unexpected number of hits.")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    if status not in ['alive', 'dead']:
        raise SomeTypeOfError("Status is an unexpected value.")

    # he's either alive or dead
    return status == "alive"

Opcja 3: Gdzieś pomiędzy

Myślę, że druga metoda, z wyjątkami, jest lepsza, ale nie jestem pewien, czy prawidłowo używam wyjątków. Jestem ciekawy, jak poradzą sobie z tym bardziej doświadczeni programiści.

jme
źródło

Odpowiedzi:

17

Zaleceniem w Pythonie jest stosowanie wyjątków w celu wskazania niepowodzenia. Jest to prawdą, nawet jeśli regularnie oczekujesz porażki.

Spójrz na to z perspektywy osoby wywołującej Twój kod:

my_status = get_abe_status(my_url)

Co jeśli zwrócimy Brak? Jeśli program wywołujący nie zajmuje się przypadkiem, w którym nie powiodło się get_abe_status, po prostu spróbuje kontynuować, używając my_stats jako None. Może to później spowodować trudny do zdiagnozowania błąd. Nawet jeśli zaznaczysz Brak, ten kod nie ma pojęcia, dlaczego metoda get_abe_status () nie powiodła się.

A co jeśli podniesiemy wyjątek? Jeśli osoba dzwoniąca nie obsługuje konkretnie sprawy, wyjątek będzie propagowany w górę, ostatecznie uderzając w domyślną procedurę obsługi wyjątków. To może nie być to, czego chcesz, ale jest lepsze niż wprowadzenie subtelnego błędu w innym miejscu programu. Ponadto wyjątek zawiera informacje o tym, co poszło źle, co zostało utracone w pierwszej wersji.

Z perspektywy dzwoniącego wygodniej jest uzyskać wyjątek niż wartość zwracaną. I to jest styl Pythona, aby używać wyjątków, aby wskazać warunki niepowodzenia, a nie zwracać wartości.

Niektórzy przyjmą inną perspektywę i argumentują, że powinieneś stosować wyjątki tylko w przypadkach, w których tak naprawdę nigdy się nie spodziewasz. Twierdzą, że normalnie działający bieg nie powinien powodować żadnych wyjątków. Jednym z podanych powodów jest to, że wyjątki są rażąco nieefektywne, ale tak naprawdę nie jest tak w przypadku Pythona.

Kilka punktów w twoim kodzie:

try:
    hits[0]
except IndexError:
    raise NotFoundError("No mentions found.")

To naprawdę mylący sposób sprawdzenia pustej listy. Nie wprowadzaj wyjątków tylko po to, by coś sprawdzić. Użyj if.

# say we expect four hits...
if len(hits) != 4:
    raise Warning("An unexpected number of hits.")
    logger.warning("An unexpected number of hits.")

Zdajesz sobie sprawę, że linia logger.warning nigdy nie będzie działać poprawnie?

Winston Ewert
źródło
1
Dziękuję (z opóźnieniem) za odpowiedź. To, wraz z przeglądaniem opublikowanego kodu, poprawiło moje wyczucie, kiedy i jak zgłosić wyjątek.
jme
4

Przyjęta odpowiedź zasługuje na akceptację i odpowiada na pytanie, piszę to tylko po to, aby zapewnić trochę dodatkowego tła.

Jednym z credo Pythona jest: łatwiej prosić o wybaczenie niż o pozwolenie. Oznacza to, że zazwyczaj po prostu robisz różne rzeczy, a jeśli oczekujesz wyjątków, radzisz sobie z nimi. W przeciwieństwie do robienia czeków przed ręką, aby upewnić się, że nie otrzymasz wyjątku.

Chcę podać przykład pokazujący, jak dramatyczna jest różnica w mentalności od C ++ / Java. Pętla for w C ++ zwykle wygląda mniej więcej tak:

for(int i = 0; i != myvector.size(); ++i) ...

Sposób na przemyślenie tego: dostęp do miejsca, w myvector[k]którym k> = myvector.size () spowoduje wyjątek. Możesz więc w zasadzie napisać to (bardzo niezręcznie) jako próbę.

    for(int i = 0; ; ++i)  {
        try {
           ...
        } catch (& std::out_of_range)
             break

Lub coś podobnego. Teraz zastanów się, co się dzieje w pętli python for:

for i in range(1):
    ...

Jak to działa? Pętla for bierze wynik z zakresu (1) i wywołuje iter (), chwytając do niego iterator.

b = range(1).__iter__()

Następnie wywołuje go przy każdej iteracji pętli, dopóki ...:

>>> next(b)
0
>>> next(b)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Innymi słowy, pętla for w pythonie jest tak naprawdę próbą-z wyjątkiem w przebraniu.

Jeśli chodzi o konkretne pytanie, pamiętaj, że wyjątki zatrzymują normalne wykonywanie funkcji i należy je rozwiązać osobno. W Pythonie powinieneś swobodnie je rzucać, ilekroć nie ma sensu wykonywać reszty kodu w twojej funkcji i / lub żaden zwrot nie poprawnie odzwierciedla tego, co się stało w funkcji. Zauważ, że wczesne wracanie z funkcji jest inne: wczesne zwracanie oznacza, że ​​już wymyśliłeś odpowiedź i nie potrzebujesz reszty kodu, aby znaleźć odpowiedź. Mówię, że wyjątki należy zgłaszać, gdy odpowiedź nie jest znana, a reszty kodu określającego odpowiedź nie można rozsądnie uruchomić. Teraz „prawidłowe odzwierciedlenie” samego siebie, podobnie jak to, które wyjątki wybierzesz, jest kwestią dokumentacji.

W przypadku twojego konkretnego kodu powiedziałbym, że każda sytuacja, która powoduje, że trafienia są pustą listą, powinna rzucić. Dlaczego? Cóż, sposób, w jaki twoja funkcja jest skonfigurowana, nie ma sposobu na określenie odpowiedzi bez analizowania trafień. Więc jeśli trafień nie można przeanalizować, albo dlatego, że adres URL jest zły, albo dlatego, że trafienia są puste, funkcja nie może odpowiedzieć na pytanie, a tak naprawdę nie może nawet próbować.

W tym konkretnym przypadku argumentowałbym, że nawet jeśli uda ci się przeanalizować i nie uzyskasz rozsądnej odpowiedzi (żywej lub martwej), nadal powinieneś rzucić. Dlaczego? Ponieważ funkcja zwraca wartość logiczną. Zwrot None Brak jest bardzo niebezpieczny dla Twojego klienta. Jeśli sprawdzą, czy nie ma opcji Brak, nie wystąpi awaria, będzie ona po prostu cicho traktowana jako Fałsz. Tak więc twój klient w zasadzie zawsze będzie musiał sprawdzić, czy „None is None” i czy nie chce cichych awarii… więc prawdopodobnie powinieneś po prostu rzucić.

Nir Friedman
źródło
2

Powinieneś stosować wyjątki, gdy dzieje się coś wyjątkowego . Oznacza to, że coś nie powinno się zdarzyć przy właściwym użyciu aplikacji. Jeśli konsument Twojej metody może i szuka czegoś, co nie zostanie znalezione, wówczas „nie znaleziono” nie jest wyjątkowym przypadkiem. W takim przypadku powinieneś zwrócić null lub „None” lub {} albo coś wskazującego na pusty zestaw zwrotów.

Z drugiej strony, jeśli naprawdę oczekujesz, że konsumenci twojej metody zawsze (chyba że jakoś spieprzą) znajdą to, co jest wyszukiwane, to nie znalezienie tego byłoby wyjątkiem i powinieneś to zrobić.

Kluczem jest to, że obsługa wyjątków może być kosztowna - wyjątki mają na celu zebranie informacji o stanie twojej aplikacji w momencie ich wystąpienia, takich jak ślad stosu, aby pomóc ludziom w odszyfrowaniu przyczyny ich wystąpienia. Nie sądzę, że to właśnie próbujesz zrobić.

Matthew Flynn
źródło
1
Jeśli zdecydujesz, że nie można znaleźć wartości, uważaj na to, czego używasz, aby wskazać, że tak się stało. Jeśli twoja metoda ma zwrócić a, Stringa jako wskaźnik wybrałeś „Brak”, oznacza to, że musisz uważać, aby „Brak” nigdy nie był prawidłową wartością. Zauważ też, że istnieje różnica między spojrzeniem na dane a nie znalezieniem wartości i niemożnością odzyskania danych, dlatego nie możemy znaleźć danych. Osiągnięcie tego samego wyniku dla tych dwóch przypadków oznacza, że ​​nie masz widoczności, gdy nie otrzymujesz żadnej wartości, gdy spodziewasz się, że będzie.
unholysampler
Wewnętrzne bloki kodu są oznaczone backtickami (`), być może właśnie to chciałeś zrobić z„ None ”?
Izkata,
3
Obawiam się, że w Pythonie jest to absolutnie nieprawda. Stosujesz rozumowanie w stylu C ++ / Java w innym języku. Python używa wyjątków do wskazania końca pętli for; to dość nietypowe.
Nir Friedman
2

Gdybym pisał funkcję

 def abe_is_alive():

Napiszę to do return Truelub Falsew przypadkach, w których jestem absolutnie pewien jednego lub drugiego, a raisebłąd w każdym innym przypadku (np raise ValueError("Status neither 'dead' nor 'alive'").). Wynika to z faktu, że funkcja wywołująca mój oczekuje wartości logicznej, a jeśli nie mogę zapewnić tego z pewnością, zwykły przepływ programu nie powinien być kontynuowany.

Coś w rodzaju twojego przykładu uzyskania innej liczby „trafień” niż oczekiwano, prawdopodobnie zignoruję; dopóki jeden z hitów nadal pasuje do mojego wzoru „Abe Vigoda jest {dead | alive}”, to w porządku. Pozwala to na zmianę kolejności strony, ale nadal otrzymuje odpowiednie informacje.

Zamiast

try:
    hits[0] 
except IndexError:
    raise NotFoundError

Chciałbym wyraźnie sprawdzić:

if not hits:
    raise NotFoundError

ponieważ jest to zwykle „tańsze”, niż konfiguracja try.

Zgadzam się z tobą IOError; Nie starałbym się również błędnie obsługiwać łączenia ze stroną internetową - jeśli z jakiegoś powodu nie możemy, to nie jest odpowiednie miejsce do obsługi tego (ponieważ nie pomaga nam to odpowiedzieć na nasze pytanie) i powinno przejść do funkcji wywoływania.

jonrsharpe
źródło