`if key in dict` vs` try / except` - quel est l'idiome le plus lisible?

93

J'ai une question sur les idiomes et la lisibilité, et il semble y avoir un choc des philosophies Python pour ce cas particulier:

Je veux construire le dictionnaire A à partir du dictionnaire B. Si une clé spécifique n'existe pas dans B, ne rien faire et continuer.

Quelle est la meilleure façon?

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

ou

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

"Faire et demander pardon" vs "simplicité et explicitation".

Quel est le meilleur et pourquoi?

LeeMobile
la source
1
Le deuxième exemple pourrait être mieux écrit comme if "blah" in B.keys(), ou if B.has_key("blah").
girasquid
2
ne A.update(B)fonctionne pas pour vous?
SilentGhost du
21
@Luke: has_keya été déprécié en faveur de inet la vérification B.keys()change une opération O (1) en une opération O (n).
kindall
4
@Luke: non, ce n'est pas le cas. .has_keyest obsolète et keyscrée une liste inutile dans py2k, et est redondant dans py3k
SilentGhost
2
'build' A, comme dans, A est vide pour commencer? Et nous ne voulons que certaines clés? Utilisez une compréhension: A = dict((k, v) for (k, v) in B if we_want_to_include(k)).
Karl Knechtel

Réponses:

76

Les exceptions ne sont pas conditionnelles.

La version conditionnelle est plus claire. C'est naturel: il s'agit d'un contrôle de flux simple, pour lequel les conditions sont conçues, pas d'exceptions.

La version d'exception est principalement utilisée comme optimisation lors de ces recherches dans une boucle: pour certains algorithmes, elle permet d'éliminer les tests des boucles internes. Cela n'a pas cet avantage ici. Il a le petit avantage d'éviter d'avoir à dire "blah"deux fois, mais si vous en faites beaucoup, vous devriez probablement avoir une move_keyfonction d' assistance de toute façon.

En général, je vous recommande fortement de vous en tenir à la version conditionnelle par défaut, sauf si vous avez une raison spécifique de ne pas le faire. Les conditions sont le moyen évident de le faire, ce qui est généralement une forte recommandation de préférer une solution à une autre.

Glenn Maynard
la source
3
Je ne suis pas d'accord. Si vous dites "faites X, et si cela ne fonctionne pas, faites Y". Raison principale contre la solution conditionnelle ici, vous devez écrire "blah"plus souvent, ce qui conduit à une situation plus sujette aux erreurs.
glglgl
6
Et, surtout en Python, EAFP est très largement utilisé.
glglgl
8
Cette réponse serait correcte pour toutes les langues que je connais, à l'exception de Python.
Tomáš Zato - Réintégrer Monica
3
Si vous utilisez des exceptions comme si elles étaient conditionnelles en Python, j'espère que personne d'autre n'aura à le lire.
Glenn Maynard
Alors, quel est le verdict final? :)
floatingpurr
60

Il existe également un troisième moyen qui évite à la fois les exceptions et la double recherche, ce qui peut être important si la recherche est coûteuse:

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

Dans le cas où vous vous attendez à ce que le dictionnaire contienne des Nonevaleurs, vous pouvez utiliser des constantes plus ésotériques comme NotImplemented, Ellipsisou en créer une nouvelle:

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

Quoi qu'il en soit, l'utilisation update()est l'option la plus lisible pour moi:

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

D'après ce que j'ai compris, vous souhaitez mettre à jour le dict A avec des paires clé et valeur de dict B

update est un meilleur choix.

A.update(B)

Exemple:

>>> 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
la source
"Si une clé spécifique n'existe pas dans B" Désolé, cela aurait dû être plus clair, mais je veux uniquement copier des valeurs si des clés spécifiques dans B existent. Not all in B.
LeeMobile
1
@LeeMobile -A.update({k: v for k, v in B.iteritems() if k in specificset})
Omnifarious
8

Citation directe du wiki de performance Python:

Sauf pour la première fois, chaque fois qu'un mot est vu, le test de l'instruction if échoue. Si vous comptez un grand nombre de mots, plusieurs se produiront probablement plusieurs fois. Dans une situation où l'initialisation d'une valeur ne se produira qu'une seule fois et où l'augmentation de cette valeur se produira plusieurs fois, il est moins coûteux d'utiliser une instruction try.

Il semble donc que les deux options soient viables selon la situation. Pour plus de détails, vous pouvez consulter ce lien: Try-except-performance

Sami Lehtinen
la source
c'est une lecture intéressante, mais je pense un peu incomplète. Le dict utilisé n'a qu'un seul élément et je soupçonne que les dicts plus grands auront un impact significatif sur les performances
user2682863
3

Je pense que la règle générale ici est d' A["blah"]exister normalement, si c'est le cas, essayez-sauf est bon sinon utilisezif "blah" in b:

Je pense que "essayer" est bon marché dans le temps mais "sauf" est plus cher.

Neil
la source
10
N'abordez pas le code du point de vue de l'optimisation par défaut; l'approche du point de vue de la lisibilité et de la maintenabilité. À moins que l'objectif ne soit spécifiquement l'optimisation, ce sont les mauvais critères (et s'il s'agit d'une optimisation, la réponse est une analyse comparative, pas une supposition).
Glenn Maynard
J'aurais probablement dû mettre le dernier point entre crochets ou en quelque sorte plus vague - mon point principal était le premier et je pense qu'il a l'avantage supplémentaire du second.
neil
3

Je pense que le deuxième exemple est ce que vous devriez choisir à moins que ce code n'ait du sens:

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

Gardez à l'esprit que le code sera abandonné dès qu'il y aura une clé qui n'y est pas B. Si ce code a du sens, vous devez utiliser la méthode d'exception, sinon utilisez la méthode de test. À mon avis, parce qu'elle est plus courte et exprime clairement l'intention, elle est beaucoup plus facile à lire que la méthode d'exception.

Bien sûr, les personnes qui vous disent d'utiliser updateont raison. Si vous utilisez une version de Python qui prend en charge la compréhension du dictionnaire, je préférerais fortement ce code:

updateset = {'foo', 'bar', 'baz'}
A.update({k: B[k] for k in updateset if k in B})
Très varié
la source
"Gardez à l'esprit que le code sera abandonné dès qu'il y aura une clé qui n'est pas dans B." - c'est pourquoi il est préférable de ne mettre que le minimum absolu dans try: block, il s'agit généralement d'une seule ligne. Le premier exemple serait mieux dans le cadre d'une boucle, commefor key in ["foo", "bar", "baz"]: try: A[key] = B[key]
Zim
2

La règle dans les autres langues est de réserver des exceptions pour des conditions exceptionnelles, c'est-à-dire des erreurs qui ne se produisent pas en utilisation régulière. Je ne sais pas comment cette règle s'applique à Python, car StopIteration ne devrait pas exister par cette règle.

Mark Ransom
la source
Je pense que cette châtaigne est originaire de langues où la gestion des exceptions est coûteuse et peut donc avoir un impact significatif sur les performances. Je n'ai jamais vu de véritable justification ou de raisonnement derrière cela.
John La Rooy
@JohnLaRooy Non, les performances ne sont pas vraiment la raison. Les exceptions sont une sorte de goto non local , que certains considèrent comme une entrave à la lisibilité du code. Cependant, l'utilisation d'exceptions de cette manière est considérée comme idiomatique en Python, donc ce qui précède ne s'applique pas.
Ian Goldpar
les retours conditionnels sont également des "goto non locaux" et beaucoup de gens préfèrent ce style au lieu d'inspecter les sentinelles à la fin du bloc de code.
cowbert
1

Personnellement, je penche vers la deuxième méthode (mais en utilisant has_key):

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

De cette façon, chaque opération d'affectation ne comporte que deux lignes (au lieu de 4 avec try / except), et toutes les exceptions levées seront de vraies erreurs ou des choses que vous avez manquées (au lieu d'essayer simplement d'accéder à des clés qui ne sont pas là) .

En fin de compte (voir les commentaires sur votre question), has_keyest obsolète - donc je suppose que c'est mieux écrit comme

if "blah" in B:
  A["blah"] = B["blah"]
girasquid
la source
1

En commençant Python 3.8, et en introduisant les expressions d'affectation (PEP 572) ( :=opérateur), nous pouvons capturer la valeur de la condition dictB.get('hello', None)dans une variable valueafin à la fois de vérifier si ce n'est pas le cas None(comme dict.get('hello', None)renvoie soit la valeur associée ou None) et de l'utiliser ensuite dans le corps de la condition:

# dictB = {'hello': 5, 'world': 42}
# dictA = {}
if value := dictB.get('hello', None):
  dictA["hello"] = value
# dictA is now {'hello': 5}
Xavier Guihot
la source
Cela échoue si valeur == 0
Eric
0

Bien que l'accent mis sur le principe "regardez avant de sauter" puisse s'appliquer à la plupart des langages, une approche plus pythonique pourrait être la première approche, basée sur les principes de python. Sans oublier qu'il s'agit d'un style de codage légitime en python. La chose importante est de vous assurer que vous utilisez le bloc try except dans le bon contexte et que vous suivez les meilleures pratiques. Par exemple. faire trop de choses dans un bloc try, attraper une exception très large, ou pire - la clause d'exception nue, etc.

Plus facile de demander pardon que la permission. (EAFP)

Voir la référence de la documentation python ici .

En outre, ce blog de Brett, l'un des principaux développeurs, aborde la plupart de cela en bref.

Voir une autre discussion SO ici :

UN J
la source