Diccionarios y valores predeterminados

213

Suponiendo que connectionDetailses un diccionario de Python, ¿cuál es la mejor, más elegante y más "pitónica" forma de refactorizar un código como este?

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

Respuestas:

311

Me gusta esto:

host = connectionDetails.get('host', someDefaultValue)
MattH
fuente
40
Tenga en cuenta que el segundo argumento es un valor, no una clave.
Marcin
77
+1 para legibilidad, pero if/elsees mucho más rápido. Eso podría o no jugar un papel.
Tim Pietzcker
77
@Tim, ¿puede proporcionar una referencia de por qué if/elsees más rápido?
nishantjr
2
@Tim: asumí que una de las ventajas de usar un lenguaje de nivel superior es que el intérprete podría 'ver' dentro de las funciones y optimizarlo, y que el usuario no tendría que lidiar tanto con las micro optimizaciones . ¿No es para eso para lo que son la compilación JIT?
nishantjr
3
@nishantjr: Python (al menos CPython, la variante más común) no tiene compilación JIT. PyPy podría resolver esto más rápido, pero no lo tengo instalado ya que Python estándar siempre ha sido lo suficientemente rápido para mis propósitos hasta ahora. En general, es poco probable que importe en la vida real: si necesita hacer
cálculos
99

También puede usar el me defaultdictgusta así:

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

Puede pasar cualquier función ordinaria en lugar de lambda:

from collections import defaultdict
def a():
  return 4

b = defaultdict(a, key="some_value")
b['absent'] => 4
b['key'] => "some_value"
tamerlaha
fuente
77
Vine aquí por un problema diferente al de la pregunta del OP, y su solución lo resuelve exactamente.
0xc0de
Lo haría +1, pero lamentablemente no encaja con getmétodos similares.
0xc0de
Esta respuesta me fue útil para asegurar que las adiciones a un diccionario incluyeran claves predeterminadas. Mi implementación es demasiado larga para describirla en una respuesta de StackOverflow, así que escribí sobre esto aquí. persagen.com/2020/03/05/…
Victoria Stuart
24

Si bien .get()es un buen lenguaje, es más lento que if/else(y más lento que try/exceptsi la presencia de la clave en el diccionario se puede esperar la mayor parte del tiempo):

>>> 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
fuente
3
Todavía no veo por qué if/then sería más rápido. Ambos casos requieren una búsqueda de diccionario, ya menos que la invocación de get()es manera mucho más lenta, lo que las cuentas de los demás por la desaceleración?
Jens
1
@Jens: las llamadas a funciones son caras.
Tim Pietzcker
1
¿Cuál no debería ser un gran problema en un diccionario muy poblado, correcto? Lo que significa que la llamada de función no va a importar mucho si la búsqueda real es costosa. Probablemente solo importa en los ejemplos de juguetes.
AturSams
2
@zehelvion: la búsqueda de diccionario es O(1)independiente del tamaño del diccionario, por lo que la sobrecarga de llamadas de función es relevante.
Tim Pietzcker,
35
es extraño si la sobrecarga de llamar a una función te haría decidir no usar get. Use lo que sus compañeros de equipo pueden leer mejor.
Jochen Bedersdorfer
19

Para múltiples valores predeterminados diferentes, intente esto:

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
fuente
3
Esta es una buena solución idiomática, pero hay una trampa. Pueden producirse resultados inesperados si se suministra connectionDetails Noneo la emptyString como uno de los valores en los pares clave-valor. El defaultsdiccionario podría potencialmente tener uno de sus valores borrados involuntariamente. (ver también stackoverflow.com/questions/6354436 )
dreftymac
9

Hay un método en los diccionarios de Python para hacer esto: dict.setdefault

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

Sin embargo, este método establece el valor de connectionDetails['host']a someDefaultValuesi la clave aún hostno está definida, a diferencia de lo que se hizo la pregunta.

Sriram
fuente
1
Tenga en cuenta que setdefault()el valor devoluciones, así que esto funciona así: host = connectionDetails.setdefault('host', someDefaultValue). Solo tenga en cuenta que se establecerá connectionDetails['host']en el valor predeterminado si la clave no estaba allí antes.
ash108
7

(esta es una respuesta tardía)

Una alternativa es subclasificar la dictclase e implementar el __missing__()método, así:

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

Ejemplos:

>>> 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
fuente
4

Al probar la sospecha de @Tim Pietzcker sobre la situación en PyPy (5.2.0-alpha0) para Python 3.3.5, encuentro que, de hecho, ambos .get()y if/ elseway funcionan de manera similar. En realidad, parece que en el caso if / else hay incluso una sola búsqueda si la condición y la asignación involucran la misma clave (compárese con el último caso donde hay dos búsquedas).

>>>> 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
Hasta que
fuente
1

Puede usar una función lamba para esto como una línea. Cree un nuevo objeto al connectionDetails2que se acceda como una función ...

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

Ahora usa

connectionDetails2(k)

en vez de

connectionDetails[k]

que devuelve el valor del diccionario si kestá en las claves; de lo contrario, devuelve"DEFAULT"

Bobak Hashemi
fuente
Te voté pero el problema con tu solución es que los dictos funcionan con [] pero las lambdas funcionan con ()
yukashima huksay