Tengo un diccionario anidado. ¿Hay una sola forma de obtener valores de forma segura?
try:
example_dict['key1']['key2']
except KeyError:
pass
¿O tal vez Python tiene un método como get()
para diccionario anidado?
fuente
Tengo un diccionario anidado. ¿Hay una sola forma de obtener valores de forma segura?
try:
example_dict['key1']['key2']
except KeyError:
pass
¿O tal vez Python tiene un método como get()
para diccionario anidado?
Puedes usar get
dos veces:
example_dict.get('key1', {}).get('key2')
Esto volverá None
si existe key1
o key2
no existe.
Tenga en cuenta que esto aún podría generar un AttributeError
if example_dict['key1']
, pero no es un dict (o un objeto similar a un dict con un get
método). El try..except
código que publicó generaría un TypeError
lugar si no example_dict['key1']
se puede suscribir.
Otra diferencia es que los try...except
cortocircuitos inmediatamente después de la primera clave faltante. La cadena de get
llamadas no.
Si desea preservar la sintaxis, example_dict['key1']['key2']
pero no desea que aumente nunca KeyErrors, puede usar la receta Hasher :
class Hasher(dict):
# https://stackoverflow.com/a/3405143/190597
def __missing__(self, key):
value = self[key] = type(self)()
return value
example_dict = Hasher()
print(example_dict['key1'])
# {}
print(example_dict['key1']['key2'])
# {}
print(type(example_dict['key1']['key2']))
# <class '__main__.Hasher'>
Tenga en cuenta que esto devuelve un Hasher vacío cuando falta una clave.
Dado que Hasher
es una subclase de dict
usted, puede usar un Hasher de la misma manera que podría usar un dict
. Todos los mismos métodos y sintaxis están disponibles, Hashers solo trata las claves faltantes de manera diferente.
Puede convertir una regular dict
en una Hasher
como esta:
hasher = Hasher(example_dict)
y convierta a Hasher
a regular con la dict
misma facilidad:
regular_dict = dict(hasher)
Otra alternativa es ocultar la fealdad en una función auxiliar:
def safeget(dct, *keys):
for key in keys:
try:
dct = dct[key]
except KeyError:
return None
return dct
Entonces, el resto de su código puede permanecer relativamente legible:
safeget(example_dict, 'key1', 'key2')
safeget
muchos sentidos, el método no es muy seguro, ya que sobrescribe el diccionario original, lo que significa que no puede hacer cosas con seguridad safeget(dct, 'a', 'b') or safeget(dct, 'a')
.
safeget
nunca sobrescribe el diccionario original. Devolverá el diccionario original, un valor del diccionario original o None
.
dct = dct[key]
reasigna un nuevo valor a la variable local dct
. Esto no muta el dict original (por lo que el dict original no se ve afectado por safeget
). Si, por otro lado, se dct[key] = ...
hubiera utilizado, entonces el dict original se habría modificado. En otras palabras, en Python los nombres están vinculados a valores . Asignación de un nuevo valor a un nombre no afecta el valor de edad (a menos que ya no hay más referencias al antiguo valor, en cuyo caso (en CPython) que conseguirá basura recogida.)
También podría usar python reduce :
def deep_get(dictionary, *keys):
return reduce(lambda d, key: d.get(key) if d else None, keys, dictionary)
Al combinar todas estas respuestas aquí y los pequeños cambios que hice, creo que esta función sería útil. Es seguro, rápido y fácil de mantener.
def deep_get(dictionary, keys, default=None):
return reduce(lambda d, key: d.get(key, default) if isinstance(d, dict) else default, keys.split("."), dictionary)
Ejemplo:
>>> from functools import reduce
>>> def deep_get(dictionary, keys, default=None):
... return reduce(lambda d, key: d.get(key, default) if isinstance(d, dict) else default, keys.split("."), dictionary)
...
>>> person = {'person':{'name':{'first':'John'}}}
>>> print (deep_get(person, "person.name.first"))
John
>>> print (deep_get(person, "person.name.lastname"))
None
>>> print (deep_get(person, "person.name.lastname", default="No lastname"))
No lastname
>>>
deep_get({'a': 1}, "a.b")
da None
pero esperaría una excepción como KeyError
o algo más.
None
aRaise KeyError
A partir de la respuesta de Yoav, un enfoque aún más seguro:
def deep_get(dictionary, *keys):
return reduce(lambda d, key: d.get(key, None) if isinstance(d, dict) else None, keys, dictionary)
Una solución recursiva. No es el más eficiente, pero me parece un poco más legible que los otros ejemplos y no se basa en functools.
def deep_get(d, keys):
if not keys or d is None:
return d
return deep_get(d.get(keys[0]), keys[1:])
Ejemplo
d = {'meta': {'status': 'OK', 'status_code': 200}}
deep_get(d, ['meta', 'status_code']) # => 200
deep_get(d, ['garbage', 'status_code']) # => None
Una versión más pulida.
def deep_get(d, keys, default=None):
"""
Example:
d = {'meta': {'status': 'OK', 'status_code': 200}}
deep_get(d, ['meta', 'status_code']) # => 200
deep_get(d, ['garbage', 'status_code']) # => None
deep_get(d, ['meta', 'garbage'], default='-') # => '-'
"""
assert type(keys) is list
if d is None:
return default
if not keys:
return d
return deep_get(d.get(keys[0]), keys[1:], default)
Si bien el enfoque de reducción es ordenado y breve, creo que un bucle simple es más fácil de asimilar. También he incluido un parámetro predeterminado.
def deep_get(_dict, keys, default=None):
for key in keys:
if isinstance(_dict, dict):
_dict = _dict.get(key, default)
else:
return default
return _dict
Como ejercicio para comprender cómo funcionaba la reducción de una línea, hice lo siguiente. Pero en última instancia, el enfoque de bucle me parece más intuitivo.
def deep_get(_dict, keys, default=None):
def _reducer(d, key):
if isinstance(d, dict):
return d.get(key, default)
return default
return reduce(_reducer, keys, _dict)
Uso
nested = {'a': {'b': {'c': 42}}}
print deep_get(nested, ['a', 'b'])
print deep_get(nested, ['a', 'b', 'z', 'z'], default='missing')
Te sugiero que pruebes python-benedict
.
Es un dict
subclase que proporciona soporte de keypath y mucho más.
Instalación: pip install python-benedict
from benedict import benedict
example_dict = benedict(example_dict, keypath_separator='.')
ahora puede acceder a valores anidados utilizando keypath :
val = example_dict['key1.key2']
# using 'get' method to avoid a possible KeyError:
val = example_dict.get('key1.key2')
o acceder a valores anidados utilizando la lista de claves :
val = example_dict['key1', 'key2']
# using get to avoid a possible KeyError:
val = example_dict.get(['key1', 'key2'])
Está bien probado y de código abierto en GitHub :
d.get('a.b[0].c[-1]')
Una clase simple que puede ajustar un dict y recuperar en función de una clave:
class FindKey(dict):
def get(self, path, default=None):
keys = path.split(".")
val = None
for key in keys:
if val:
if isinstance(val, list):
val = [v.get(key, default) if v else None for v in val]
else:
val = val.get(key, default)
else:
val = dict.get(self, key, default)
if not val:
break
return val
Por ejemplo:
person = {'person':{'name':{'first':'John'}}}
FindDict(person).get('person.name.first') # == 'John'
Si la clave no existe, regresa None
por defecto. Puede anular eso usando una default=
clave en el FindDict
contenedor, por ejemplo`:
FindDict(person, default='').get('person.name.last') # == doesn't exist, so ''
para una recuperación de clave de segundo nivel, puede hacer esto:
key2_value = (example_dict.get('key1') or {}).get('key2')
Después de ver esto para obtener atributos profundos, hice lo siguiente para obtener dict
valores anidados de manera segura usando la notación de puntos. Esto funciona para mí porque dicts
son objetos MongoDB deserializados, así que sé que los nombres de las claves no contienen .
s. Además, en mi contexto, puedo especificar un valor de respaldo falso ( None
) que no tengo en mis datos, por lo que puedo evitar el patrón try / except al llamar a la función.
from functools import reduce # Python 3
def deepgetitem(obj, item, fallback=None):
"""Steps through an item chain to get the ultimate value.
If ultimate value or path to value does not exist, does not raise
an exception and instead returns `fallback`.
>>> d = {'snl_final': {'about': {'_icsd': {'icsd_id': 1}}}}
>>> deepgetitem(d, 'snl_final.about._icsd.icsd_id')
1
>>> deepgetitem(d, 'snl_final.about._sandbox.sbx_id')
>>>
"""
def getitem(obj, name):
try:
return obj[name]
except (KeyError, TypeError):
return fallback
return reduce(getitem, item.split('.'), obj)
fallback
en realidad no se usa en la función.
.
sep=','
palabra clave arg para generalizar para determinadas condiciones (sep, fallback). Y @denvar, si obj
se dice de tipo int
después de una secuencia de reducción, entonces obj [nombre] genera un TypeError, que atrapo. Si usé obj.get (name) u obj.get (name, fallback) en su lugar, generaría un AttributeError, por lo que de cualquier manera tendría que atraparlo.
Otra función para la misma cosa, también devuelve un valor booleano para representar si se encontró la clave o no y maneja algunos errores inesperados.
'''
json : json to extract value from if exists
path : details.detail.first_name
empty path represents root
returns a tuple (boolean, object)
boolean : True if path exists, otherwise False
object : the object if path exists otherwise None
'''
def get_json_value_at_path(json, path=None, default=None):
if not bool(path):
return True, json
if type(json) is not dict :
raise ValueError(f'json={json}, path={path} not supported, json must be a dict')
if type(path) is not str and type(path) is not list:
raise ValueError(f'path format {path} not supported, path can be a list of strings like [x,y,z] or a string like x.y.z')
if type(path) is str:
path = path.strip('.').split('.')
key = path[0]
if key in json.keys():
return get_json_value_at_path(json[key], path[1:], default)
else:
return False, default
ejemplo de uso:
my_json = {'details' : {'first_name' : 'holla', 'last_name' : 'holla'}}
print(get_json_value_at_path(my_json, 'details.first_name', ''))
print(get_json_value_at_path(my_json, 'details.phone', ''))
(Cierto, 'holla')
(Falso '')
Puedes usar pydash:
import pydash as _
_.get(example_dict, 'key1.key2', default='Default')
Una adaptación de la respuesta de unutbu que encontré útil en mi propio código:
example_dict.setdefaut('key1', {}).get('key2')
Genera una entrada de diccionario para la clave1 si aún no tiene esa clave para evitar KeyError. Si quieres terminar un diccionario anidado que incluye ese emparejamiento de teclas de todos modos como lo hice yo, esta parece ser la solución más fácil.
Dado que es razonable hacer un error de clave si falta una de las claves, incluso no podemos verificarlo y obtenerlo de la siguiente manera:
def get_dict(d, kl):
cur = d[kl[0]]
return get_dict(cur, kl[1:]) if len(kl) > 1 else cur
Poca mejora en el reduce
enfoque para que funcione con la lista. También se utiliza la ruta de datos como cadena dividida por puntos en lugar de matriz.
def deep_get(dictionary, path):
keys = path.split('.')
return reduce(lambda d, key: d[int(key)] if isinstance(d, list) else d.get(key) if d else None, keys, dictionary)
Una solución que he usado que es similar al doble get pero con la capacidad adicional de evitar un TypeError usando la lógica if else:
value = example_dict['key1']['key2'] if example_dict.get('key1') and example_dict['key1'].get('key2') else default_value
Sin embargo, cuanto más anidado está el diccionario, más engorroso se vuelve.
Para búsquedas anidadas de diccionario / JSON, puede usar dictor
pip install dictor
objeto dict
{
"characters": {
"Lonestar": {
"id": 55923,
"role": "renegade",
"items": [
"space winnebago",
"leather jacket"
]
},
"Barfolomew": {
"id": 55924,
"role": "mawg",
"items": [
"peanut butter jar",
"waggy tail"
]
},
"Dark Helmet": {
"id": 99999,
"role": "Good is dumb",
"items": [
"Shwartz",
"helmet"
]
},
"Skroob": {
"id": 12345,
"role": "Spaceballs CEO",
"items": [
"luggage"
]
}
}
}
para obtener los elementos de Lonestar, simplemente proporcione una ruta separada por puntos, es decir
import json
from dictor import dictor
with open('test.json') as data:
data = json.load(data)
print dictor(data, 'characters.Lonestar.items')
>> [u'space winnebago', u'leather jacket']
puede proporcionar un valor de reserva en caso de que la clave no esté en la ruta
Hay muchas más opciones que puedes hacer, como ignorar el uso de mayúsculas y minúsculas y usar otros caracteres además de '.' como un separador de ruta,
Cambié poco esta respuesta. Agregué verificar si estamos usando la lista con números. Así que ahora podemos usarlo de cualquier manera. deep_get(allTemp, [0], {})
o deep_get(getMinimalTemp, [0, minimalTemperatureKey], 26)
etc.
def deep_get(_dict, keys, default=None):
def _reducer(d, key):
if isinstance(d, dict):
return d.get(key, default)
if isinstance(d, list):
return d[key] if len(d) > 0 else default
return default
return reduce(_reducer, keys, _dict)
Ya hay muchas buenas respuestas, pero se me ocurrió una función llamada get similar to lodash get en JavaScript land que también permite acceder a las listas por índice:
def get(value, keys, default_value = None):
'''
Useful for reaching into nested JSON like data
Inspired by JavaScript lodash get and Clojure get-in etc.
'''
if value is None or keys is None:
return None
path = keys.split('.') if isinstance(keys, str) else keys
result = value
def valid_index(key):
return re.match('^([1-9][0-9]*|[0-9])$', key) and int(key) >= 0
def is_dict_like(v):
return hasattr(v, '__getitem__') and hasattr(v, '__contains__')
for key in path:
if isinstance(result, list) and valid_index(key) and int(key) < len(result):
result = result[int(key)] if int(key) < len(result) else None
elif is_dict_like(result) and key in result:
result = result[key]
else:
result = default_value
break
return result
def test_get():
assert get(None, ['foo']) == None
assert get({'foo': 1}, None) == None
assert get(None, None) == None
assert get({'foo': 1}, []) == {'foo': 1}
assert get({'foo': 1}, ['foo']) == 1
assert get({'foo': 1}, ['bar']) == None
assert get({'foo': 1}, ['bar'], 'the default') == 'the default'
assert get({'foo': {'bar': 'hello'}}, ['foo', 'bar']) == 'hello'
assert get({'foo': {'bar': 'hello'}}, 'foo.bar') == 'hello'
assert get({'foo': [{'bar': 'hello'}]}, 'foo.0.bar') == 'hello'
assert get({'foo': [{'bar': 'hello'}]}, 'foo.1') == None
assert get({'foo': [{'bar': 'hello'}]}, 'foo.1.bar') == None
assert get(['foo', 'bar'], '1') == 'bar'
assert get(['foo', 'bar'], '2') == None
except keyerror:
cláusula.