ʻIf key in dict` a `try / except` - który idiom jest bardziej czytelny?

94

Mam pytanie dotyczące idiomów i czytelności i wydaje się, że w tym konkretnym przypadku dochodzi do zderzenia filozofii Pythona:

Chcę zbudować słownik A ze słownika B. Jeśli określony klucz nie istnieje w B, nie rób nic i kontynuuj.

Który sposób jest lepszy?

try:
    A["blah"] = B["blah"]
except KeyError:
    pass

lub

if "blah" in B:
    A["blah"] = B["blah"]

„Czyń i proś o przebaczenie” kontra „prostota i jednoznaczność”.

Który jest lepszy i dlaczego?

LeeMobile
źródło
1
Drugi przykład można lepiej zapisać jako if "blah" in B.keys(), lub if B.has_key("blah").
girasquid
2
nie A.update(B)nie pracować dla Ciebie?
SilentGhost
21
@Luke: has_keyzostał wycofany na korzyść ini sprawdzanie B.keys()zmienia operację O (1) na operację O (n).
kindall
4
@Luke: nie, nie jest. .has_keyjest przestarzały i keystworzy niepotrzebną listę w py2k i jest zbędny w py3k
SilentGhost
2
'build' A, jak w, A jest pusty na początek? I chcemy tylko określonych kluczy? Użyj zrozumieniem: A = dict((k, v) for (k, v) in B if we_want_to_include(k)).
Karl Knechtel

Odpowiedzi:

76

Wyjątki nie są warunkami warunkowymi.

Wersja warunkowa jest jaśniejsza. To naturalne: jest to prosta kontrola przepływu, do której służą warunki warunkowe, a nie wyjątki.

Wersja wyjątku jest używana przede wszystkim jako optymalizacja podczas wykonywania tych wyszukiwań w pętli: w przypadku niektórych algorytmów pozwala na wyeliminowanie testów z wewnętrznych pętli. Tutaj nie ma takiej korzyści. Ma tę małą zaletę, że unika konieczności "blah"dwukrotnego mówienia , ale jeśli robisz wiele z tych rzeczy, i tak prawdopodobnie powinieneś mieć funkcję pomocniczą move_key.

Ogólnie rzecz biorąc, zdecydowanie zalecam domyślne trzymanie się wersji warunkowej, chyba że masz konkretny powód, aby tego nie robić. Warunki warunkowe są oczywistym sposobem, aby to zrobić, co zwykle jest silną rekomendacją, aby preferować jedno rozwiązanie od innego.

Glenn Maynard
źródło
3
Nie zgadzam się. Jeśli powiesz „zrób X, a jeśli to nie zadziała, zrób Y”. Główny powód przeciwko warunkowemu rozwiązaniu tutaj, musisz pisać "blah"częściej, co prowadzi do sytuacji bardziej podatnej na błędy.
glglgl
6
Szczególnie w Pythonie EAFP jest bardzo szeroko stosowany.
glglgl
8
Ta odpowiedź byłaby poprawna dla każdego języka, który znam, z wyjątkiem Pythona.
Tomáš Zato - Przywróć Monikę
3
Jeśli używasz wyjątków tak, jakby były one warunkami warunkowymi w Pythonie, mam nadzieję, że nikt inny nie musi ich czytać.
Glenn Maynard,
Więc jaki jest ostateczny werdykt? :)
floatingpurr
62

Istnieje również trzeci sposób, który pozwala uniknąć zarówno wyjątków, jak i podwójnego wyszukiwania, co może być ważne, jeśli wyszukiwanie jest drogie:

value = B.get("blah", None)
if value is not None: 
    A["blah"] = value

Jeśli spodziewasz się, że słownik będzie zawierał Nonewartości, możesz użyć bardziej ezoterycznych stałych, takich jak NotImplemented, Ellipsislub utworzyć nową:

MyConst = object()
def update_key(A, B, key):
    value = B.get(key, MyConst)
    if value is not MyConst: 
        A[key] = value

W każdym razie użycie update()jest dla mnie najbardziej czytelną opcją:

a.update((k, b[k]) for k in ("foo", "bar", "blah") if k in b)
lqc
źródło
14

Z tego, co rozumiem, chcesz zaktualizować dyktę A za pomocą par klucza i wartości z dyktu B.

update to lepszy wybór.

A.update(B)

Przykład:

>>> A = {'a':1, 'b': 2, 'c':3}
>>> B = {'d': 2, 'b':5, 'c': 4}
>>> A.update(B)
>>> A
{'a': 1, 'c': 4, 'b': 5, 'd': 2}
>>> 
pyfunc
źródło
„Jeśli określony klucz nie istnieje w B” Przepraszamy, powinno być bardziej zrozumiałe, ale chcę skopiować wartości tylko wtedy, gdy istnieją określone klucze w B. Nie wszyscy w B.
LeeMobile
1
@LeeMobile -A.update({k: v for k, v in B.iteritems() if k in specificset})
Omnifarious
8

Bezpośredni cytat z wiki wydajności Pythona:

Z wyjątkiem pierwszego razu, za każdym razem, gdy widzimy słowo, test instrukcji if kończy się niepowodzeniem. Jeśli liczysz dużą liczbę słów, wiele z nich prawdopodobnie wystąpi wielokrotnie. W sytuacji, gdy inicjalizacja wartości ma nastąpić tylko raz, a zwiększenie tej wartości nastąpi wielokrotnie, tańsze jest użycie instrukcji try.

Wydaje się więc, że obie opcje są wykonalne w zależności od sytuacji. Aby uzyskać więcej informacji, możesz sprawdzić ten link: Spróbuj z wyjątkiem wydajności

Sami Lehtinen
źródło
to ciekawa lektura, ale myślę, że jest nieco niepełna. Użyty dykt ma tylko 1 element i podejrzewam, że większe dykty będą miały znaczący wpływ na wydajność
user2682863
3

Myślę, że ogólna zasada tutaj jest A["blah"]normalna, jeśli tak, spróbuj-z wyjątkiem tego, że jest dobre, jeśli nie, to użyjif "blah" in b:

Myślę, że „spróbowanie” jest tanie w czasie, ale „z wyjątkiem” jest droższe.

neil
źródło
10
Nie podchodź domyślnie do kodu z perspektywy optymalizacji; podejść do tego z perspektywy czytelności i łatwości konserwacji. O ile celem nie jest konkretnie optymalizacja, jest to niewłaściwe kryterium (a jeśli jest to optymalizacja, odpowiedzią jest analiza porównawcza, a nie zgadywanie).
Glenn Maynard
Prawdopodobnie powinienem był umieścić ostatni punkt w nawiasach lub w jakiś sposób niejasny - moim głównym punktem był pierwszy i myślę, że ma on dodatkową zaletę w stosunku do drugiego.
neil
3

Myślę, że drugi przykład jest tym, do czego powinieneś się udać, chyba że ten kod ma sens:

try:
    A["foo"] = B["foo"]
    A["bar"] = B["bar"]
    A["baz"] = B["baz"]
except KeyError:
    pass

Pamiętaj, że kod zostanie przerwany, gdy tylko pojawi się klucz, którego nie ma B . Jeśli ten kod ma sens, powinieneś użyć metody wyjątku, w przeciwnym razie użyj metody testowej. Moim zdaniem, ponieważ jest krótszy i wyraźnie wyraża zamiar, jest o wiele łatwiejszy do odczytania niż metoda wyjątku.

Oczywiście ludzie, którzy mówią, że masz używać, updatemają rację. Jeśli używasz wersji Pythona, która obsługuje wyrażenia słownikowe, zdecydowanie wolałbym ten kod:

updateset = {'foo', 'bar', 'baz'}
A.update({k: B[k] for k in updateset if k in B})
Wszelaki
źródło
„Pamiętaj, że kod zostanie przerwany, gdy tylko pojawi się klucz, którego nie ma w B.” - dlatego najlepszą praktyką jest umieszczanie w try: block tylko absolutnego minimum, zwykle jest to pojedyncza linia. Pierwszy przykład byłby lepszy jako część pętli, na przykładfor key in ["foo", "bar", "baz"]: try: A[key] = B[key]
Zim
2

Zasadą w innych językach jest zastrzeżenie wyjątków dla wyjątkowych warunków, tj. Błędów, które nie występują podczas regularnego używania. Nie wiem, jak ta reguła odnosi się do Pythona, ponieważ StopIteration nie powinien istnieć przez tę regułę.

Mark Okup
źródło
Myślę, że ten kasztan pochodzi z języków, w których obsługa wyjątków jest droga, a więc może mieć znaczący wpływ na wydajność. Nigdy nie widziałem żadnego prawdziwego uzasadnienia ani rozumowania stojącego za tym.
John La Rooy
@JohnLaRooy Nie, wydajność nie jest tak naprawdę powodem. Wyjątki to rodzaj nielokalnego goto , który niektórzy uważają za utrudniający czytelność kodu. Jednak użycie wyjątków w ten sposób jest uważane za idiomatyczne w Pythonie, więc powyższe nie ma zastosowania.
Ian Goldby
zwroty warunkowe to także „nielokalne goto” i wiele osób woli ten styl zamiast sprawdzania wartowników na końcu bloku kodu.
cowbert
1

Osobiście skłaniam się ku drugiej metodzie (ale używam has_key):

if B.has_key("blah"):
  A["blah"] = B["blah"]

W ten sposób każda operacja przypisania ma tylko dwie linie (zamiast 4 z try / except), a wszelkie wyjątki, które zostaną wyrzucone, będą prawdziwymi błędami lub rzeczami, które przegapiłeś (zamiast próbować tylko uzyskać dostęp do kluczy, których tam nie ma) .

Jak się okazuje (zobacz komentarze do twojego pytania), has_keyjest przestarzały - więc myślę, że lepiej napisać jako

if "blah" in B:
  A["blah"] = B["blah"]
girasquid
źródło
1

Rozpoczynając Python 3.8i wprowadzając wyrażenia przypisania (PEP 572) ( :=operator), możemy przechwycić wartość warunku dictB.get('hello', None)w zmiennej value, aby zarówno sprawdzić, czy tak nie jest None(ponieważ dict.get('hello', None)zwraca skojarzoną wartość lub None), a następnie użyć jej w treści warunek:

# dictB = {'hello': 5, 'world': 42}
# dictA = {}
if value := dictB.get('hello', None):
  dictA["hello"] = value
# dictA is now {'hello': 5}
Xavier Guihot
źródło
1
To się nie powiedzie, jeśli wartość == 0
Eric
0

Chociaż w przyjętej odpowiedzi podkreślenie zasady „zobacz, zanim skoczysz” może mieć zastosowanie do większości języków, bardziej Pythonowe może być pierwszym podejściem, opartym na zasadach Pythona. Nie wspominając o tym, że jest to legalny styl kodowania w Pythonie. Ważne jest, aby upewnić się, że używasz try except block w odpowiednim kontekście i postępujesz zgodnie z najlepszymi praktykami. Na przykład. robienie zbyt wielu rzeczy w bloku try, przechwytywanie bardzo szerokiego wyjątku lub, co gorsza, klauzula gołego wyjątkiem itp.

Łatwiej prosić o przebaczenie niż o pozwolenie. (EAFP)

Zobacz dokumentację Pythona tutaj .

Także to blog Bretta, jednego z głównych twórców, porusza większość tego w skrócie.

Zobacz inną dyskusję SO tutaj :

AJ
źródło