Czy zagnieżdżone bloki try / except w Pythonie to dobra praktyka programistyczna?

211

Piszę własny kontener, który musi dać dostęp do słownika wewnątrz poprzez wywołania atrybutów. Typowe zastosowanie kontenera wyglądałoby następująco:

dict_container = DictContainer()
dict_container['foo'] = bar
...
print dict_container.foo

Wiem, że pisanie czegoś takiego może być głupie, ale taką funkcjonalność muszę zapewnić. Myślałem o wdrożeniu tego w następujący sposób:

def __getattribute__(self, item):
    try:
        return object.__getattribute__(item)
    except AttributeError:
        try:
            return self.dict[item]
        except KeyError:
            print "The object doesn't have such attribute"

Nie jestem pewien, czy zagnieżdżone bloki try / except są dobrą praktyką, więc innym sposobem byłoby użycie hasattr()i has_key():

def __getattribute__(self, item):
        if hasattr(self, item):
            return object.__getattribute__(item)
        else:
            if self.dict.has_key(item):
                return self.dict[item]
            else:
                raise AttributeError("some customised error")

Lub użyj jednego z nich i jednego spróbuj złapać blok w ten sposób:

def __getattribute__(self, item):
    if hasattr(self, item):
        return object.__getattribute__(item)
    else:
        try:
            return self.dict[item]
        except KeyError:
            raise AttributeError("some customised error")

Która opcja jest najbardziej pytoniczna i elegancka?

Michał
źródło
Doceniłby dostarczenie przez bogów Pythona if 'foo' in dict_container:. Amen.
gseattle

Odpowiedzi:

194

Twój pierwszy przykład jest w porządku. Nawet oficjalna dokumentacja Pythona zaleca ten styl znany jako EAFP .

Osobiście wolę unikać zagnieżdżania, gdy nie jest to konieczne:

def __getattribute__(self, item):
    try:
        return object.__getattribute__(item)
    except AttributeError:
        pass  # fallback to dict
    try:
        return self.dict[item]
    except KeyError:
        raise AttributeError("The object doesn't have such attribute") from None

PS. has_key()przez długi czas był przestarzały w Pythonie 2. Użyj item in self.dictzamiast tego.

lqc
źródło
2
return object.__getattribute__(item)jest niepoprawny i zwróci, TypeErrorponieważ przekazywana jest zła liczba argumentów. Zamiast tego powinno być return object.__getattribute__(self, item).
martineau
13
PEP 20: płaski jest lepszy niż zagnieżdżony.
Ioannis Filippidis
8
Co from Noneoznacza w ostatnim wierszu?
niklas
2
@niklas Zasadniczo pomija kontekst wyjątku („podczas obsługi tego wyjątku wystąpił inny wyjątek” - komunikaty typu „-esque”). Zobacz tutaj
Kade,
Fakt, że dokumentacja Pythona zaleca zagnieżdżanie prób jest trochę szalony. To oczywiście horrendalny styl. Prawidłowym sposobem obsługi łańcucha operacji, w którym coś może się nie udać, byłoby użycie jakiejś konstrukcji monadycznej, której Python nie obsługuje.
Henry Henrinson
19

Podczas gdy w Javie rzeczywiście zła praktyka jest używanie wyjątków do kontroli przepływu (głównie dlatego, że wyjątki zmuszają jvm do gromadzenia zasobów ( więcej tutaj )), w Pythonie obowiązują 2 ważne zasady: Duck Typing i EAFP . Zasadniczo oznacza to, że zachęcamy do próbowania używania obiektu w sposób, w jaki myślisz, że będzie działał, i radzenia sobie, gdy rzeczy nie są takie.

Podsumowując, jedynym problemem byłoby zbyt duże wcięcie kodu. Jeśli masz na to ochotę, spróbuj uprościć niektóre zagnieżdżenia, takie jak lqc sugerowane w sugerowanej powyżej odpowiedzi .

Bruno Penteado
źródło
13

Tylko bądź ostrożny - w tym przypadku najpierw finallyjest dotykany, ALE też pomijany.

def a(z):
    try:
        100/z
    except ZeroDivisionError:
        try:
            print('x')
        finally:
            return 42
    finally:
        return 1


In [1]: a(0)
x
Out[1]: 1
Sławomir Lenart
źródło
Wow, to mnie oszałamia ... Czy możesz wskazać mi fragment dokumentacji wyjaśniający to zachowanie?
Michał
2
@Michal: fyi: oba finallybloki są wykonywane a(0), ale finally-returnzwracany jest tylko rodzic .
Sławomir Lenart
11

W twoim konkretnym przykładzie nie musisz ich zagnieżdżać. Jeśli wyrażenie w trybloku powiedzie się, funkcja zwróci, więc kod po całym bloku try / except zostanie uruchomiony tylko wtedy, gdy pierwsza próba się nie powiedzie. Możesz więc po prostu:

def __getattribute__(self, item):
    try:
        return object.__getattribute__(item)
    except AttributeError:
        pass
    # execution only reaches here when try block raised AttributeError
    try:
        return self.dict[item]
    except KeyError:
        print "The object doesn't have such attribute"

Zagnieżdżanie ich nie jest złe, ale czuję, że pozostawienie go płaskiego sprawia, że ​​struktura jest bardziej przejrzysta: po kolei próbujesz serii rzeczy i zwracasz pierwszą, która działa.

Nawiasem mówiąc, możesz pomyśleć o tym, czy naprawdę chcesz użyć __getattribute__zamiast __getattr__tutaj. Używanie __getattr__uprości sprawę, ponieważ będziesz wiedział, że normalny proces wyszukiwania atrybutów już się nie powiódł.

BrenBarn
źródło
7

Moim zdaniem byłby to najbardziej Pythonowy sposób, aby sobie z tym poradzić, chociaż i ponieważ sprawia, że ​​twoje pytanie jest dyskusyjne. Zauważ, że __getattr__()zamiast tego definiuje, __getattribute__()ponieważ oznacza to, że musi zajmować się tylko „specjalnymi” atrybutami przechowywanymi w wewnętrznym słowniku.

def __getattr__(self, name):
    """only called when an attribute lookup in the usual places has failed"""
    try:
        return self.my_dict[name]
    except KeyError:
        raise AttributeError("some customized error message")
martineau
źródło
2
Zauważ, że zgłoszenie wyjątku w exceptbloku może dać mylące dane wyjściowe w Pythonie 3. Dzieje się tak, ponieważ (zgodnie z PEP 3134) Python 3 śledzi pierwszy wyjątek (the KeyError) jako „kontekst” drugiego wyjątku (the AttributeError) i jeśli dociera do najwyższy poziom, wydrukuje śledzenie, które zawiera oba wyjątki. Może to być pomocne, gdy nie oczekiwano drugiego wyjątku, ale jeśli celowo zgłaszasz drugi wyjątek, jest to niepożądane. W przypadku Pythona 3.3 PEP 415 dodał możliwość pomijania kontekstu przy użyciu raise AttributeError("whatever") from None.
Blckknght
3
@Blckknght: Wydrukowanie śledzenia wstecznego, które zawiera oba wyjątki, byłoby w tym przypadku w porządku. Innymi słowy, nie sądzę, aby twoje ogólne stwierdzenie, że zawsze jest niepożądane, jest prawdą. W użyciu tutaj zamienia się KeyErrorw an AttributeErrori pokazanie, że to, co wydarzyło się w śledzeniu, byłoby przydatne i odpowiednie.
martineau
W bardziej skomplikowanych sytuacjach możesz mieć rację, ale myślę, że podczas konwersji między typami wyjątków często wiesz, że szczegóły pierwszego wyjątku nie mają znaczenia dla użytkownika zewnętrznego. Oznacza to, że jeśli __getattr__zgłasza wyjątek, błąd jest prawdopodobnie błędem w dostępie do atrybutu, a nie błędem implementacji w kodzie bieżącej klasy. Pokazanie wcześniejszego wyjątku jako kontekstu może to zmylić. A nawet jeśli wyłączysz kontekst za pomocą raise Whatever from None, nadal możesz w razie potrzeby uzyskać dostęp do poprzedniego wyjątku za pośrednictwem ex.__context__.
Blckknght
1
Chciałem zaakceptować twoją odpowiedź, jednak w pytaniu byłem bardziej zaciekawiony, czy użycie zagnieżdżonego bloku try / catch jest dobrą praktyką. Z drugiej strony to najbardziej eleganckie rozwiązanie i zamierzam go użyć w swoim kodzie. Wielkie dzięki Martin.
Michał
Michał: Nie ma za co. Jest też szybszy niż używanie __getattribute__().
martineau
4

Zgodnie z dokumentacją lepiej jest obsługiwać wiele wyjątków za pomocą krotek lub w ten sposób:

import sys

try:
    f = open('myfile.txt')
    s = f.readline()
    i = int(s.strip())
except IOError as e:
    print "I/O error({0}): {1}".format(e.errno, e.strerror)
except ValueError:
    print "Could not convert data to an integer."
except:
    print "Unexpected error:", sys.exc_info()[0]
    raise
Blairg23
źródło
2
Ta odpowiedź tak naprawdę nie odnosi się do pierwotnego pytania, ale dla każdego, kto ją czyta, zauważ, że "nagie", z wyjątkiem tego, że na końcu jest okropnym pomysłem (zwykle), ponieważ złapie wszystko, w tym np. NameError i KeyboardInterrupt - co zwykle nie ma na myśli!
przegrał
Biorąc pod uwagę, że kod ponownie zgłasza ten sam wyjątek zaraz po instrukcji print, czy to naprawdę wielka sprawa. W takim przypadku może zapewnić szerszy kontekst wyjątku bez jego ukrywania. Gdyby to się nie powtórzyło, zgodziłbym się całkowicie, ale nie sądzę, że istnieje ryzyko ukrywania wyjątku, którego nie zamierzałeś.
NimbusScale
4

Dobrym i prostym przykładem zagnieżdżonej try / except może być:

import numpy as np

def divide(x, y):
    try:
        out = x/y
    except:
        try:
            out = np.inf * x / abs(x)
        except:
            out = np.nan
    finally:
        return out

Teraz wypróbuj różne kombinacje, a otrzymasz poprawny wynik:

divide(15, 3)
# 5.0

divide(15, 0)
# inf

divide(-15, 0)
# -inf

divide(0, 0)
# nan

[oczywiście mamy numpy, więc nie musimy tworzyć tej funkcji]

Niszcząca opieka
źródło
2

Jedną z rzeczy, których lubię unikać, jest zgłaszanie nowego wyjątku podczas obsługi starego. Powoduje to, że odczytywanie komunikatów o błędach jest mylące.

Na przykład w moim kodzie pierwotnie napisałem

try:
    return tuple.__getitem__(self, i)(key)
except IndexError:
    raise KeyError(key)

I dostałem tę wiadomość.

>>> During handling of above exception, another exception occurred.

To, czego chciałem, to:

try:
    return tuple.__getitem__(self, i)(key)
except IndexError:
    pass
raise KeyError(key)

Nie ma to wpływu na sposób obsługi wyjątków. W każdym bloku kodu zostałby przechwycony KeyError. To tylko kwestia zdobywania punktów za styl.

Steve Zelaznik
źródło
1
Zobacz jednak użycie podbicia w zaakceptowanej odpowiedzi from None, aby uzyskać jeszcze więcej punktów za styl. :)
Pianosaurus
1

Jeśli try-oprócz-final jest zagnieżdżony w bloku last, wynik z „dziecka” zostaje ostatecznie zachowany. Nie znalazłem jeszcze oficjalnego wyjaśnienia, ale poniższy fragment kodu pokazuje to zachowanie w Pythonie 3.6.

def f2():
    try:
        a = 4
        raise SyntaxError
    except SyntaxError as se:
        print('log SE')
        raise se from None
    finally:
        try:
            raise ValueError
        except ValueError as ve:
            a = 5
            print('log VE')
            raise ve from None
        finally:
            return 6       
        return a

In [1]: f2()
log SE
log VE
Out[2]: 6
Guanghua Shu
źródło
To zachowanie różni się od przykładu podanego przez @ Sławomira Lenarta, gdy ostatecznie jest zagnieżdżony wewnątrz oprócz bloku.
Guanghua Shu,
0

Nie sądzę, żeby to była kwestia bycia pytonicznym czy eleganckim. Chodzi o to, aby jak najbardziej zapobiegać wyjątkom. Wyjątki mają na celu obsługę błędów, które mogą wystąpić w kodzie lub zdarzeniach, nad którymi nie masz kontroli. W takim przypadku masz pełną kontrolę podczas sprawdzania, czy element jest atrybutem czy w słowniku, więc unikaj zagnieżdżonych wyjątków i trzymaj się drugiej próby.

owobeid
źródło
1
Z dokumentacji: W środowisku wielowątkowym podejście LBYL (Look Before You Leap) może grozić wprowadzeniem wyścigu między „patrzeniem” a „skakaniem”. Na przykład kod, jeśli key in mapping: return mapping [key] może się nie powieść, jeśli inny wątek usunie klucz z mapowania po teście, ale przed wyszukiwaniem. Ten problem można rozwiązać za pomocą zamków lub stosując podejście
Nuno André,