`if key in dict` vs.` try / tranne` - qual è l'idioma più leggibile?


93

Ho una domanda sugli idiomi e la leggibilità, e sembra che ci sia uno scontro tra le filosofie Python per questo caso particolare:

Voglio costruire il dizionario A dal dizionario B. Se una chiave specifica non esiste in B, non fare nulla e continuare.

Qual è il modo migliore?

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

o

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

"Fai e chiedi perdono" contro "semplicità e chiarezza".

Qual è il migliore e perché?


1
Il secondo esempio potrebbe essere scritto meglio come if "blah" in B.keys(), o if B.has_key("blah").
girasquid

2
non A.update(B)funziona per te?
SilentGhost

21
@Luke: has_keyè stato sconsigliato ine il controllo B.keys()cambia un'operazione O (1) in una O (n).
kindall

4
@Luke: non lo è non. .has_keyè deprecato e keyscrea un elenco non necessario in py2k ed è ridondante in py3k
SilentGhost

2
'build' A, come in, A è vuoto per cominciare? E vogliamo solo determinate chiavi? Utilizzare la comprensione: A = dict((k, v) for (k, v) in B if we_want_to_include(k)).
Karl Knechtel

Risposte:


75

Le eccezioni non sono condizionali.

La versione condizionale è più chiara. È naturale: questo è un controllo di flusso diretto, che è ciò per cui sono progettati i condizionali, non le eccezioni.

La versione dell'eccezione viene utilizzata principalmente come ottimizzazione quando si eseguono queste ricerche in un ciclo: per alcuni algoritmi consente di eliminare i test dai cicli interni. Non ha quel vantaggio qui. Ha il piccolo vantaggio di evitare di doverlo dire "blah"due volte, ma se stai facendo molti di questi probabilmente dovresti comunque avere una move_keyfunzione di aiuto .

In generale, consiglio vivamente di attenersi alla versione condizionale per impostazione predefinita a meno che tu non abbia un motivo specifico per non farlo. I condizionali sono il modo più ovvio per farlo, che di solito è una forte raccomandazione per preferire una soluzione rispetto a un'altra.


3
Non sono d'accordo Se dici "fai X, e se non funziona, fai Y". Motivo principale contro la soluzione condizionale qui, devi scrivere "blah"più spesso, il che porta a una situazione più soggetta a errori.
glglgl

6
E, soprattutto in Python, EAFP è molto utilizzato.
glglgl

8
Questa risposta sarebbe corretta per qualsiasi lingua che conosco ad eccezione di Python.
Tomáš Zato - Ripristina Monica il

3
Se stai usando le eccezioni come se fossero condizionali in Python, spero che nessun altro debba leggerlo.
Glenn Maynard

Allora, qual è il verdetto finale? :)
floatingpurr

61

C'è anche un terzo modo che evita sia le eccezioni che la doppia ricerca, che può essere importante se la ricerca è costosa:

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

Nel caso in cui ti aspetti che il dizionario contenga Nonevalori, puoi usare alcune costanti più esoteriche come NotImplemented, Ellipsiso crearne uno nuovo:

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

Ad ogni modo, l'utilizzo update()è l'opzione più leggibile per me:

a.update((k, b[k]) for k in ("foo", "bar", "blah") if k in b)

14

Da quello che ho capito, vuoi aggiornare il dict A con la chiave, le coppie di valori dal dict B.

update è una scelta migliore.

A.update(B)

Esempio:

>>> 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}
>>> 

"Se una chiave specifica non esiste in B" Spiacenti, avrebbe dovuto essere più chiaro, ma voglio solo copiare i valori se esistono chiavi specifiche in B. Non tutti in B.
LeeMobile

1
@LeeMobile -A.update({k: v for k, v in B.iteritems() if k in specificset})
Omnifarious

8

Citazione diretta dal wiki delle prestazioni di Python:

Tranne che per la prima volta, ogni volta che viene vista una parola il test dell'istruzione if fallisce. Se stai contando un numero elevato di parole, molte probabilmente si verificheranno più volte. In una situazione in cui l'inizializzazione di un valore avverrà solo una volta e l'aumento di quel valore si verificherà molte volte, è più economico utilizzare un'istruzione try.

Quindi sembra che entrambe le opzioni siano praticabili a seconda della situazione. Per maggiori dettagli potresti controllare questo link: Try-tranne-performance


è una lettura interessante, ma penso in qualche modo incompleta. Il dict utilizzato ha solo 1 elemento e sospetto che i dict più grandi avranno un impatto significativo sulle prestazioni
user2682863

3

Penso che la regola generale qui A["blah"]esisterà normalmente, in tal caso prova, tranne che è buono, altrimenti usaif "blah" in b:

Penso che "provare" sia economico nel tempo, ma "tranne" è più costoso.


10
Non affrontare il codice da una prospettiva di ottimizzazione per impostazione predefinita; affrontarlo da una prospettiva di leggibilità e manutenibilità. A meno che l'obiettivo non sia specificamente l'ottimizzazione, questo è il criterio sbagliato (e se è l'ottimizzazione, la risposta è il benchmarking, non la supposizione).
Glenn Maynard

Probabilmente avrei dovuto mettere l'ultimo punto tra parentesi o in qualche modo più vago - il mio punto principale era il primo e penso che abbia il vantaggio aggiuntivo del secondo.
neil

3

Penso che il secondo esempio sia quello che dovresti cercare a meno che questo codice non abbia senso:

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

Tieni presente che il codice verrà interrotto non appena c'è una chiave che non è in B . Se questo codice ha senso, è necessario utilizzare il metodo di eccezione, altrimenti utilizzare il metodo di test. A mio parere, poiché è più breve ed esprime chiaramente l'intento, è molto più facile da leggere rispetto al metodo delle eccezioni.

Naturalmente, le persone che ti dicono di usare updatehanno ragione. Se stai usando una versione di Python che supporta la comprensione del dizionario, preferirei fortemente questo codice:

updateset = {'foo', 'bar', 'baz'}
A.update({k: B[k] for k in updateset if k in B})

"Tieni presente che il codice verrà interrotto non appena c'è una chiave che non è in B." - questo è il motivo per cui è buona norma mettere solo il minimo assoluto nel blocco try:, di solito questa è una singola riga. Il primo esempio sarebbe meglio come parte di un ciclo, comefor key in ["foo", "bar", "baz"]: try: A[key] = B[key]
Zim

2

La regola in altre lingue è di riservare eccezioni per condizioni eccezionali, cioè errori che non si verificano durante l'uso regolare. Non so come quella regola si applichi a Python, poiché StopIteration non dovrebbe esistere secondo quella regola.


Penso che questa castagna abbia avuto origine da lingue in cui la gestione delle eccezioni è costosa e quindi può avere un impatto significativo sulle prestazioni. Non ho mai visto alcuna vera giustificazione o ragionamento dietro di esso.
John La Rooy

@JohnLaRooy No, le prestazioni non sono proprio il motivo. Le eccezioni sono una sorta di goto non locale , che alcune persone considerano impedire la leggibilità del codice. Tuttavia, l'uso di eccezioni in questo modo è considerato idiomatico in Python, quindi quanto sopra non si applica.
Ian Goldby

I ritorni condizionali sono anche "goto non locali" e molte persone preferiscono quello stile invece di ispezionare le sentinelle alla fine del blocco di codice.
cowbert

1

Personalmente, propendo per il secondo metodo (ma utilizzando has_key):

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

In questo modo, ogni operazione di assegnazione è solo di due righe (invece di 4 con try / tranne), e tutte le eccezioni che vengono lanciate saranno errori reali o cose che ti sei perso (invece di provare ad accedere a chiavi che non ci sono) .

A quanto pare (vedi i commenti alla tua domanda), has_keyè deprecato, quindi immagino che sia meglio scritto come

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

1

A partire Python 3.8e con l'introduzione delle espressioni di assegnazione (PEP 572) ( :=operatore), possiamo catturare il valore della condizione dictB.get('hello', None)in una variabile valuein modo da verificare se non lo è None(poiché dict.get('hello', None)restituisce il valore associato o None) e quindi utilizzarlo all'interno del corpo di la condizione:

# dictB = {'hello': 5, 'world': 42}
# dictA = {}
if value := dictB.get('hello', None):
  dictA["hello"] = value
# dictA is now {'hello': 5}

Questo fallisce se value == 0
Eric

0

Sebbene l'enfasi della risposta accettata sul principio "guarda prima di saltare" possa applicarsi alla maggior parte delle lingue, il primo approccio potrebbe essere più pitonico, basato sui principi di Python. Per non parlare del fatto che è uno stile di codifica legittimo in Python. La cosa importante è assicurarsi di utilizzare il blocco try tranne nel giusto contesto e seguire le migliori pratiche. Per esempio. fare troppe cose in un blocco try, catturare un'eccezione molto ampia o, peggio, la clausola nuda tranne ecc.

Più facile chiedere perdono che permesso. (EAFP)

Vedi il riferimento alla documentazione di Python qui .

Inoltre, questo blog di Brett, uno degli sviluppatori principali, tocca la maggior parte di questo in breve.

Vedi un'altra discussione SO qui :

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.