Dictionnaires et valeurs par défaut

213

En supposant qu'il connectionDetailss'agit d'un dictionnaire Python, quelle est la façon la plus élégante, la plus "pythonique" de refactoriser du code comme celui-ci?

if "host" in connectionDetails:
    host = connectionDetails["host"]
else:
    host = someDefaultValue
mnowotka
la source

Réponses:

311

Comme ça:

host = connectionDetails.get('host', someDefaultValue)
MattH
la source
40
Notez que le deuxième argument est une valeur, pas une clé.
Marcin
7
+1 pour la lisibilité, mais if/elseest beaucoup plus rapide. Cela pourrait ou non jouer un rôle.
Tim Pietzcker
7
@Tim, pouvez-vous fournir une référence expliquant pourquoi if/elseest plus rapide?
nishantjr
2
@Tim: J'avais supposé que l'un des avantages de l'utilisation d'un langage de niveau supérieur était que l'interprète serait capable de «voir» à l'intérieur des fonctions et de l'optimiser - que l'utilisateur n'aurait pas autant à faire face aux micro-optimisations . N'est-ce pas à cela que servent des choses comme la compilation JIT?
nishantjr
3
@nishantjr: Python (au moins CPython, la variante la plus courante) n'a pas de compilation JIT. PyPy pourrait en effet résoudre ce problème plus rapidement, mais je ne l'ai pas installé car le Python standard a toujours été assez rapide pour mes besoins jusqu'à présent. En général, il est peu probable que cela ait de l'importance dans la vie réelle - si vous avez besoin de faire un calcul de nombre critique en temps, Python n'est probablement pas le langage de choix ...
Tim Pietzcker
99

Vous pouvez également utiliser des éléments defaultdictsimilaires:

from collections import defaultdict
a = defaultdict(lambda: "default", key="some_value")
a["blabla"] => "default"
a["key"] => "some_value"

Vous pouvez passer n'importe quelle fonction ordinaire au lieu de lambda:

from collections import defaultdict
def a():
  return 4

b = defaultdict(a, key="some_value")
b['absent'] => 4
b['key'] => "some_value"
tamerlaha
la source
7
Je suis venu ici pour un problème différent de la question du PO, et votre solution le résout exactement.
0xc0de
Je voudrais le +1, mais malheureusement, cela ne correspond pas à getdes méthodes similaires.
0xc0de
Cette réponse m'a été utile pour garantir que les ajouts à un dictionnaire comprenaient des clés par défaut. Mon implémentation est un peu trop longue à décrire dans une réponse StackOverflow, j'ai donc écrit à ce sujet ici. persagen.com/2020/03/05/…
Victoria Stuart
24

Bien que ce .get()soit un bel idiome, il est plus lent que if/else(et plus lent que try/exceptsi la présence de la clé dans le dictionnaire peut être attendue la plupart du temps):

>>> timeit.timeit(setup="d={1:2, 3:4, 5:6, 7:8, 9:0}", 
... stmt="try:\n a=d[1]\nexcept KeyError:\n a=10")
0.07691968797894333
>>> timeit.timeit(setup="d={1:2, 3:4, 5:6, 7:8, 9:0}", 
... stmt="try:\n a=d[2]\nexcept KeyError:\n a=10")
0.4583777282275605
>>> timeit.timeit(setup="d={1:2, 3:4, 5:6, 7:8, 9:0}", 
... stmt="a=d.get(1, 10)")
0.17784020746671558
>>> timeit.timeit(setup="d={1:2, 3:4, 5:6, 7:8, 9:0}", 
... stmt="a=d.get(2, 10)")
0.17952161730158878
>>> timeit.timeit(setup="d={1:2, 3:4, 5:6, 7:8, 9:0}", 
... stmt="if 1 in d:\n a=d[1]\nelse:\n a=10")
0.10071221458065338
>>> timeit.timeit(setup="d={1:2, 3:4, 5:6, 7:8, 9:0}", 
... stmt="if 2 in d:\n a=d[2]\nelse:\n a=10")
0.06966537335119938
Tim Pietzcker
la source
3
Je ne vois toujours pas pourquoi if/then serait plus rapide. Les deux cas ont besoin d' une recherche dans le dictionnaire, et à moins que l'invocation de get()est donc beaucoup plus lent, ce qui compte le reste du ralentissement?
Jens
1
@Jens: Les appels de fonction sont chers.
Tim Pietzcker
1
Ce qui ne devrait pas être un gros problème dans un dictionnaire très peuplé, n'est-ce pas? Cela signifie que l'appel de fonction n'aura pas beaucoup d'importance si la recherche réelle est coûteuse. Cela n'a probablement d'importance que dans les exemples de jouets.
AturSams
2
@zehelvion: La recherche dans le dictionnaire est O(1)indépendamment de la taille du dictionnaire, donc la surcharge d'appel de fonction est pertinente.
Tim Pietzcker
35
il est bizarre que la surcharge de l'appel d'une fonction vous fasse décider de ne pas utiliser get. Utilisez ce que vos collègues de l'équipe savent le mieux lire.
Jochen Bedersdorfer
19

Pour plusieurs valeurs par défaut différentes, essayez ceci:

connectionDetails = { "host": "www.example.com" }
defaults = { "host": "127.0.0.1", "port": 8080 }

completeDetails = {}
completeDetails.update(defaults)
completeDetails.update(connectionDetails)
completeDetails["host"]  # ==> "www.example.com"
completeDetails["port"]  # ==> 8080
Jerome Baum
la source
3
C'est une bonne solution idiomatique, mais il y a un piège. Des résultats inattendus peuvent se produire si connectionDetails est fourni avec Noneou la emptyString comme l'une des valeurs des paires clé-valeur. Le defaultsdictionnaire pourrait potentiellement avoir une de ses valeurs supprimée involontairement. (voir aussi stackoverflow.com/questions/6354436 )
dreftymac
9

Il existe une méthode dans les dictionnaires python pour ce faire: dict.setdefault

connectionDetails.setdefault('host',someDefaultValue)
host = connectionDetails['host']

Cependant , cette méthode permet de définir la valeur connectionDetails['host']à someDefaultValuesi la clé hostest pas déjà défini, contrairement à ce que la question posée.

Sriram
la source
1
Notez que la setdefault()valeur de retour, donc cela fonctionne aussi bien: host = connectionDetails.setdefault('host', someDefaultValue). Gardez juste à l'esprit qu'il sera réglé connectionDetails['host']sur la valeur par défaut si la clé n'était pas là auparavant.
ash108
7

(c'est une réponse tardive)

Une alternative consiste à sous- dictclasser la classe et à implémenter la __missing__()méthode, comme ceci:

class ConnectionDetails(dict):
    def __missing__(self, key):
        if key == 'host':
            return "localhost"
        raise KeyError(key)

Exemples:

>>> connection_details = ConnectionDetails(port=80)

>>> connection_details['host']
'localhost'

>>> connection_details['port']
80

>>> connection_details['password']
Traceback (most recent call last):
  File "python", line 1, in <module>
  File "python", line 6, in __missing__
KeyError: 'password'
Laurent LAPORTE
la source
4

En testant les soupçons de @Tim Pietzcker sur la situation dans PyPy (5.2.0-alpha0) pour Python 3.3.5, je trouve qu'en effet les deux .get()et le if/ elseway fonctionnent de manière similaire. En fait, il semble que dans le cas if / else, il n'y a même qu'une seule recherche si la condition et l'affectation impliquent la même clé (comparer avec le dernier cas où il y a deux recherches).

>>>> timeit.timeit(setup="d={1:2, 3:4, 5:6, 7:8, 9:0}",
.... stmt="try:\n a=d[1]\nexcept KeyError:\n a=10")
0.011889292989508249
>>>> timeit.timeit(setup="d={1:2, 3:4, 5:6, 7:8, 9:0}",
.... stmt="try:\n a=d[2]\nexcept KeyError:\n a=10")
0.07310474599944428
>>>> timeit.timeit(setup="d={1:2, 3:4, 5:6, 7:8, 9:0}",
.... stmt="a=d.get(1, 10)")
0.010391917996457778
>>>> timeit.timeit(setup="d={1:2, 3:4, 5:6, 7:8, 9:0}",
.... stmt="a=d.get(2, 10)")
0.009348208011942916
>>>> timeit.timeit(setup="d={1:2, 3:4, 5:6, 7:8, 9:0}",
.... stmt="if 1 in d:\n a=d[1]\nelse:\n a=10")
0.011475925013655797
>>>> timeit.timeit(setup="d={1:2, 3:4, 5:6, 7:8, 9:0}",
.... stmt="if 2 in d:\n a=d[2]\nelse:\n a=10")
0.009605801998986863
>>>> timeit.timeit(setup="d={1:2, 3:4, 5:6, 7:8, 9:0}",
.... stmt="if 2 in d:\n a=d[2]\nelse:\n a=d[1]")
0.017342638995614834
Jusqu'à
la source
1

Vous pouvez utiliser une fonction lamba pour cela comme une ligne. Créez un nouvel objet connectionDetails2accessible comme une fonction ...

connectionDetails2 = lambda k: connectionDetails[k] if k in connectionDetails.keys() else "DEFAULT"

Maintenant, utilisez

connectionDetails2(k)

au lieu de

connectionDetails[k]

qui retourne la valeur du dictionnaire si kest dans les clés, sinon il retourne"DEFAULT"

Bobak Hashemi
la source
Je vous ai voté mais le problème avec votre solution est que les dict fonctionnent avec [] mais les lambdas fonctionnent avec ()
yukashima huksay