Python - 'if foo in dict' vs 'try: dict [foo]'

24

C'est moins une question sur la nature de la frappe de canard que sur la façon de rester pythonique, je suppose.

Tout d'abord - lorsqu'il s'agit de dictés, en particulier lorsque la structure du dict est assez prévisible et qu'une clé donnée n'est généralement pas présente mais l'est parfois, je pense d'abord à deux approches:

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

Et bien sûr, l'approche du pardon contre la permission de Ye Olde.

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

En tant que compagnon gars Python, j'ai l'impression que je vois ce dernier beaucoup préféré, ce qui ne me semble étrange que parce que dans les documents Python try/exceptssemblent être préférés quand il y a une erreur réelle, par opposition à une, euh ... absence de succès?

Si le dict occasionnel n'a pas de clé dans myDict, et on sait qu'il n'aura pas toujours cette clé, un essai / sauf contextuellement trompeur? Ce n'est pas une erreur de programmation, c'est juste un fait des données - ce dict n'a tout simplement pas cette clé particulière.

Cela semble particulièrement important lorsque vous regardez la syntaxe try / except / else, qui semble être vraiment utile lorsqu'il s'agit de s'assurer que try ne détecte pas trop d'erreurs. Vous pouvez faire quelque chose comme:

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

Cela ne va-t-il pas conduire à avaler toutes sortes d'erreurs étranges qui sont probablement le résultat d'un mauvais code? Le code ci-dessus pourrait simplement vous empêcher de voir que vous essayez d'ajouter 2 + {}et vous ne réaliserez peut-être jamais qu'une partie de votre code a horriblement mal tourné. Je ne suggère pas que nous devrions vérifier tous les types, c'est pourquoi c'est Python et non JavaScript - mais encore une fois avec le contexte de try / except, il semble qu'il est censé attraper le programme en train de faire quelque chose qu'il ne devrait pas faire, à la place de lui permettre de continuer.

Je me rends compte que l'exemple ci-dessus est quelque chose d'un argument de l'homme de paille, et est en fait intentionnellement mauvais. Mais étant donné le credo pythonique de, better to ask forgiveness than permissionje ne peux m'empêcher de penser que cela pose la question de savoir où se situe réellement la ligne entre l'application correcte de if / else vs try / except, en particulier lorsque vous savez à quoi vous attendre de les données avec lesquelles vous travaillez.

Je ne parle même pas des problèmes de vitesse ou des meilleures pratiques ici, je suis juste un peu confus par le diagramme Venn perçu des cas où il semble que cela pourrait aller dans les deux sens, mais les gens se trompent du côté d'un essai / sauf parce que «quelqu'un quelque part a dit que c'était Pythonic». Ai-je tiré des conclusions erronées sur l'application de cette syntaxe?


la source
Je conviens que la tendance à essayer, sauf risque de cacher des erreurs. Mon avis est que vous devriez généralement éviter les essais, sauf pour les conditions que vous attendez (bien sûr, certaines exceptions s'appliquent à cette règle).
Gordon Bean

Réponses:

26

Utilisez plutôt la méthode get :

some_dict.get(the_key, default_value)

... où default_valueest la valeur renvoyée si the_keyn'est pas dans some_dict. Si vous omettez default_value, Noneest retourné si la clé est manquante.

En général, en Python, les gens ont tendance à préférer essayer / sauf plutôt que de vérifier d'abord quelque chose - voir l' entrée EAFP dans le glossaire . Notez que de nombreuses fonctions de "test d'appartenance" utilisent des exceptions en arrière-plan.

detly
la source
Mais n'est-ce pas le même problème? Si nous essayons de travailler avec un dictet de faire un travail basé sur ce qui est trouvé, il semble que if myDict.get(a_key) is not None: do_work(myDict[a_key])c'est plus verbeux et encore un autre exemple de recherche implicite avant de sauter - en plus c'est à mon humble avis légèrement moins lisible. Peut-être que je ne comprends pas dict.get(a_key, a_default)dans le bon contexte, mais il semble que ce soit un précurseur de l'écriture de 'not-a-switch-statement if / elses' ou de valeurs magiques. J'aime bien ce que fait cette méthode, je vais approfondir.
1
@Stick - vous faites:, data = myDict.get(a_key, default)et soit vous do_workferez la bonne chose une fois donné default(par exemple. None) Soit vous le ferez if data: do_work(data). Une seule recherche est nécessaire dans les deux cas. (Il se peut if data is not None: ..., dans les deux cas, qu'il ne s'agisse que d'une seule recherche et que votre propre logique puisse prendre le relais.)
Detly
1
J'ai donc décidé que dict.get () m'a pratiquement épargné une tonne d'efforts dans mon projet actuel. Cela a réduit mon nombre de lignes, nettoyé beaucoup de try/except/elsedéchets indésirables et a globalement amélioré à mon humble avis la lisibilité de mon code. J'ai appris une leçon précieuse que j'avais entendue auparavant mais j'ai réussi à négliger de prendre à cœur: "Si vous avez l'impression d'écrire trop de code, arrêtez-vous et regardez les fonctionnalités du langage." Merci!!
@Stick - content de l'entendre :) J'aime ce conseil, je ne l'ai jamais entendu auparavant.
detly
1
Techniquement, lequel est le plus rapide?
Olivier Pons le
4

La clé manquante est-elle une règle ou une exception ?

D'un point de vue pragmatique, lancer / attraper est beaucoup plus lent que de vérifier si la clé est dans le tableau. Si la clé manquante est un phénomène courant - vous devez utiliser la condition.

Une autre chose en faveur de la condition est une clause else - il est beaucoup plus facile de comprendre quand un bloc est exécuté, sachez que même la même classe d'exception peut être levée à partir de plusieurs instructions dans le bloc try.

Le code dans la clause except ne devrait pas faire partie de la faille normale (par exemple si la clé n'est pas là, ajoutez-la) - il devrait gérer l'erreur.

Eugene
la source
4
"D'un point de vue pragmatique, lancer / attraper est beaucoup plus lent que de vérifier si la clé est dans le tableau" - non. Pas nécessairement de toute façon. Dernière vérification, les implémentations Python les plus courantes font la version try/ catchplus rapidement.
detly
2
Lancer / attraper en python n'est pas si grave en termes de performances, dans la mesure où il est souvent utilisé pour le contrôle de flux en python. En outre, il existe la possibilité dans certaines situations où ce dict pourrait être modifié entre le test et l'accès.
whatsisname
@whatsisname - J'ai pensé ajouter cela à ma réponse, mais TBH si vous avez des dangers de course comme ça, vous avez des problèmes beaucoup plus gros que EAFP vs LBYL: P
détesté le
1

Dans l'esprit d'être "pythonique" et, plus précisément, de taper du canard - le try / except me semble presque fragile.

Tout ce que vous voulez vraiment, c'est quelque chose avec un accès de type dict, mais vous __getitem__pouvez le remplacer pour faire quelque chose qui ne vous convient pas. Par exemple, en utilisant defaultdictla bibliothèque standard:

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

Comme la valeur de retour par défaut est Falsy, la vérification d'accès comme celle-ci fonctionnera très bien la plupart du temps.

Malheureusement, quelques mois plus tard, ces clés supplémentaires ont fini par provoquer des problèmes et des bogues difficiles à retrouver, car elles sont 0devenues une valeur valide. Et parfois , nous pourrions utiliser inpour vérifier si une clé existe, ce qui rend le code faire des choses différentes quand il aurait été identique.

La raison pour laquelle je suggère que cela semble cassé est que, dans l'esprit de Python de la frappe de canard, tout ce que vous devriez vouloir est quelque chose de semblable à un dict, et defaultdictrépond parfaitement à cette exigence, comme le ferait tout objet que vous pourriez créer qui hérite de dict. Vous ne pouvez pas garantir que l'appelant ne changera pas son implémentation plus tard, vous devriez donc faire la chose avec le moins d'effets secondaires.

Utilisez la première version if myKey in mydict,.


Notez également qu'il s'agit spécifiquement de l'exemple de la question. Le credo en python de "Mieux vaut demander pardon que permission" est largement destiné à garantir que vous agissez réellement correctement lorsque vous n'obtenez pas ce que vous voulez. Par exemple, vérifier si un fichier existe et essayer de le lire - au moins 3 choses auxquelles je peux penser du haut de ma tête peuvent mal tourner:

  • Une condition de concurrence est que le fichier ait pu être supprimé / déplacé / etc
  • Vous n'avez pas d'autorisations de lecture sur le fichier
  • Le fichier est corrompu

Le premier sur lequel vous pouvez essayer de "demander la permission", mais par nature des conditions de course, cela ne fonctionnera pas nécessairement toujours; le second est trop facile à oublier de vérifier; et le troisième ne peut pas être vérifié sans essayer de lire le fichier. Dans tous les cas, ce que vous ferez après avoir échoué sera presque certainement le même, donc dans cet exemple, il est préférable de "demander pardon" en utilisant un try / except.

Izkata
la source