Python - „if foo in dict” vs „try: dict [foo]”

24

Jest to mniej pytanie o naturę pisania kaczek, a raczej o pozostawanie pythonem, jak sądzę.

Po pierwsze - w przypadku dykt, w szczególności gdy struktura dykt jest dość przewidywalna, a dany klucz nie jest zwykle obecny, ale czasami tak jest, najpierw myślę o dwóch podejściach:

if myKey in dict:
    do_some_work(dict[myKey])
else:
    pass

I oczywiście podejście Ye Olde „wybaczenie vs pozwolenie”.

try:
    do_some_work(dict[myKey])
except KeyError:
    pass

Jako czeladnik Python, wydaje mi się, że widzę, że ten drugi bardzo go preferuje, co wydaje mi się dziwne, ponieważ w Pythonie dokumenty try/exceptswydają się preferowane, gdy występuje rzeczywisty błąd, a nie… um… brak sukcesu?

Jeśli okazjonalny dykta nie ma klucza w myDict i wiadomo, że nie zawsze będzie miał ten klucz, to czy próba / próba kontekstu wprowadza w błąd? To nie jest błąd programistyczny, to tylko fakt danych - ten dykta po prostu nie miał tego konkretnego klucza.

Wydaje się to szczególnie ważne, gdy spojrzysz na składnię try / wyjątek / else, która wydaje się bardzo przydatna, jeśli chodzi o upewnienie się, że try nie wykryje zbyt wielu błędów. Możesz zrobić coś takiego:

try:
    foo += bar
except TypeError:
    pass
else:
    return some_more_work(foo)

Czy to nie doprowadzi do przełknięcia wszelkiego rodzaju dziwnych błędów, które prawdopodobnie są wynikiem złego kodu? Powyższy kod może po prostu uniemożliwić zauważenie, że próbujesz dodać, 2 + {}i możesz nigdy nie zdawać sobie sprawy, że pewna część kodu poszła strasznie źle. Nie sugeruję, że powinniśmy sprawdzać wszystkie typy, dlatego jest to Python, a nie JavaScript - ale znowu w kontekście try / wyjątkiem wydaje się, że powinien złapać program robiąc coś, czego nie powinien, zamiast tego umożliwienia kontynuacji.

Zdaję sobie sprawę, że powyższy przykład jest czymś w rodzaju kłótni i rzeczywiście jest celowo zły. Ale biorąc pod uwagę pytonowe wyznanie wiary, better to ask forgiveness than permissionnie mogę przestać czuć, że rodzi się pytanie, gdzie linia na piasku faktycznie znajduje się między poprawnym zastosowaniem if / else vs try / wyjątek, w szczególności gdy wiesz, czego się spodziewać po dane, z którymi pracujesz.

Nie mówię tu nawet o problemach z szybkością ani o najlepszych praktykach, jestem po prostu trochę zdezorientowany przez postrzegany schemat Venna przypadków, w których wygląda na to, że może pójść w obie strony, ale ludzie mylą się po stronie próby / z wyjątkiem ponieważ „ktoś gdzieś powiedział, że to Python”. Czy wyciągnąłem błędne wnioski na temat zastosowania tej składni?


źródło
Zgadzam się, że tendencja do wypróbowywania, z wyjątkiem ryzyka ukrywania błędów. Uważam, że ogólnie powinieneś unikać try-try, z wyjątkiem warunków, które spodziewasz się zobaczyć (oczywiście niektóre wyjątki dotyczą tej reguły).
Gordon Bean

Odpowiedzi:

26

Zamiast tego użyj metody get :

some_dict.get(the_key, default_value)

... gdzie default_valuejest zwracana wartość, jeśli the_keynie jest w some_dict. Jeśli pominiesz default_value, Nonejest zwracany, jeśli brakuje klucza.

Ogólnie rzecz biorąc, w Pythonie ludzie wolą próbować / z wyjątkiem sprawdzania czegoś najpierw - zobacz wpis EAFP w glosariuszu . Zauważ, że wiele funkcji „test członkostwa” używa wyjątków za kulisami.

obrzydliwie
źródło
Czy to jednak nie jest ten sam problem? Jeśli próbujemy pracować z dictdziałaniem opartym na tym, co zostało znalezione, wydaje się, że jest if myDict.get(a_key) is not None: do_work(myDict[a_key])to bardziej wyraziste i kolejny przykład niejawnego patrzenia przed przeskakiwaniem - a ponadto jest to nieco mniej czytelne IMHO. Być może nie rozumiem dict.get(a_key, a_default)we właściwym kontekście, ale wydaje się, że jest to prekursor do napisania „instrukcji non-switch-if if / elses” lub wartości magicznych. Podoba mi się to, co robi ta metoda, przyjrzę się temu głębiej.
1
@Stick - robisz: data = myDict.get(a_key, default)i albo do_workzrobisz właściwą rzecz, gdy otrzymasz default(np. None) Lub zrobisz if data: do_work(data). W obu przypadkach potrzebne jest tylko jedno wyszukiwanie. (W if data is not None: ...obu przypadkach może to być tylko jedno wyszukiwanie, a wtedy Twoja logika może przejąć kontrolę).
detly
1
Zdecydowałem więc, że dict.get () zaoszczędził mi mnóstwo wysiłku w moim obecnym projekcie. Zmniejszyło to moją liczbę wierszy, wyczyściło wiele okropnie wyglądających try/except/elseśmieci i właśnie poprawiło IMHO czytelność mojego kodu. Nauczyłem się cennej lekcji, którą słyszałem wcześniej, ale zaniedbałem wzięcie sobie do serca: „Jeśli masz wrażenie, że piszesz za dużo kodu, przestań i spójrz na funkcje językowe”. Dzięki!!
@Stick - miło to słyszeć :) Podoba mi się ta rada, nigdy wcześniej jej nie słyszałem.
detly
1
Technicznie, który z nich jest najszybszy?
Olivier Pons
4

Czy brakujący klucz jest regułą czy wyjątkiem ?

Z pragmatycznego punktu widzenia rzucanie / łapanie jest znacznie wolniejsze niż sprawdzanie, czy klucz znajduje się w tabeli. Jeśli brakujący klucz jest częstym zjawiskiem - powinieneś użyć warunku.

Kolejną rzeczą na korzyść warunku jest klauzula else - o wiele łatwiej jest zrozumieć, kiedy którykolwiek blok zostanie wykonany, należy wziąć pod uwagę, że nawet ta sama klasa wyjątków może zostać wyrzucona z kilku instrukcji w bloku try.

Kod w klauzuli oprócz nie powinien być częścią normalnej wady (np. Jeśli klucza nie ma, dodaj go) - powinien obsługiwać błąd.

Eugene
źródło
4
„Z pragmatycznego punktu widzenia rzucanie / łapanie jest znacznie wolniejsze niż sprawdzanie, czy klucz znajduje się w tabeli” - nie, nie jest. W każdym razie niekoniecznie. Ostatnio sprawdziłem, że najpopularniejsze implementacje Pythona wykonują wersję try/ catchszybciej.
detly
2
Rzucanie / łapanie w pythonie nie jest aż tak duże, jeśli chodzi o wydajność transakcji, do tego stopnia, że ​​często jest używane do kontroli przepływu w pythonie. Dodatkowo istnieje możliwość w niektórych sytuacjach, w których można zmodyfikować ten słownik między testem a dostępem.
whatsisname
@whatsisname - Pomyślałem o dodaniu tego do mojej odpowiedzi, ale TBH, jeśli masz takie zagrożenia rasowe, masz znacznie większe problemy niż EAFP vs LBYL: P
detly
1

W duchu bycia „pytonicznym”, a zwłaszcza pisaniem kaczek - try / wyjątkiem wydaje mi się krucha.

Wszystko, czego naprawdę chcesz, to coś z dostępem przypominającym dyktowanie, ale __getitem__możesz to zmienić, aby zrobić coś niezupełnie, czego oczekujesz. Na przykład przy użyciu defaultdictze standardowej biblioteki:

In [1]: from collections import defaultdict

In [2]: foo = defaultdict(int)

In [3]: foo['a'] = 1

In [4]: foo['b']  # Someone did access checking with a try/except exactly as you have in the question
Out[4]: 0

In [5]: 'a' in foo
Out[5]: True

In [6]: 'b' in foo  # We never set it, but now it exists!
Out[6]: True

In [7]: 'c' in foo
Out[7]: False

Ponieważ domyślną wartością zwracaną jest Falsy, sprawdzanie dostępu w ten sposób będzie działać przez większość czasu w porządku. Wygląda na to, że kod jest również prostszy, i dlatego ja i moi współpracownicy robiliśmy to od miesięcy.

Niestety, kilka miesięcy później te dodatkowe klucze faktycznie spowodowały problemy i trudne do wyśledzenia błędy, ponieważ 0stały się prawidłową wartością. A czasami byłoby użyć indo sprawdzenia, czy istnieje klucz, dzięki czemu kod robić różne rzeczy, kiedy to powinna być identyczna.

Powodem, dla którego sugeruję, że wygląda na zepsuty, jest to, że w duchu Pythona polegającego na pisaniu kaczek wszystko, czego powinieneś chcieć, to coś podobnego do dyktowania, i defaultdictpasuje idealnie do tego wymogu, podobnie jak każdy obiekt, z którego można by dziedziczyć dict. Nie możesz zagwarantować, że osoba dzwoniąca nie zmieni jej implementacji później, więc powinieneś zrobić to z najmniejszymi efektami ubocznymi.

Użyj pierwszej wersji if myKey in mydict.


Zauważ też, że chodzi konkretnie o przykład w pytaniu. Wyznanie Pythona „Lepiej prosić o wybaczenie niż pozwolenie” ma w dużej mierze na celu zapewnienie, że faktycznie postępujesz właściwie, gdy nie dostaniesz tego, czego chcesz. Na przykład sprawdzenie, czy plik istnieje, a następnie próba odczytania go - co najmniej 3 rzeczy, które mogę wymyślić z góry głowy, mogą pójść nie tak:

  • Warunkiem wyścigu jest to, że plik mógł zostać usunięty / przeniesiony / itp
  • Nie masz uprawnień do odczytu pliku
  • Plik został uszkodzony

Pierwszy, w którym możesz spróbować „poprosić o pozwolenie”, ale z uwagi na warunki wyścigowe niekoniecznie zawsze zadziała; drugi jest zbyt łatwy do zapomnienia, aby sprawdzić; a trzeciego nie można sprawdzić bez próby odczytania pliku. W każdym razie, co robisz po nie prawie na pewno będzie taka sama, więc w tym przykładzie, że jest lepiej „prosić o przebaczenie” za pomocą try / except.

Izkata
źródło