¿Acceder a elementos de diccionario anidados a través de una lista de claves?

143

Tengo una estructura de diccionario compleja a la que me gustaría acceder a través de una lista de claves para abordar el elemento correcto.

dataDict = {
    "a":{
        "r": 1,
        "s": 2,
        "t": 3
        },
    "b":{
        "u": 1,
        "v": {
            "x": 1,
            "y": 2,
            "z": 3
        },
        "w": 3
        }
}    

maplist = ["a", "r"]

o

maplist = ["b", "v", "y"]

He creado el siguiente código que funciona, pero estoy seguro de que hay una manera mejor y más eficiente de hacerlo si alguien tiene una idea.

# Get a given data from a dictionary with position provided as a list
def getFromDict(dataDict, mapList):    
    for k in mapList: dataDict = dataDict[k]
    return dataDict

# Set a given data in a dictionary with position provided as a list
def setInDict(dataDict, mapList, value): 
    for k in mapList[:-1]: dataDict = dataDict[k]
    dataDict[mapList[-1]] = value
kolergy
fuente

Respuestas:

230

Use reduce()para recorrer el diccionario:

from functools import reduce  # forward compatibility for Python 3
import operator

def getFromDict(dataDict, mapList):
    return reduce(operator.getitem, mapList, dataDict)

y reutilizar getFromDictpara encontrar la ubicación para almacenar el valor de setInDict():

def setInDict(dataDict, mapList, value):
    getFromDict(dataDict, mapList[:-1])[mapList[-1]] = value

mapListSe necesita todo menos el último elemento para encontrar el diccionario 'padre' para agregar el valor, luego use el último elemento para establecer el valor en la clave correcta.

Manifestación:

>>> getFromDict(dataDict, ["a", "r"])
1
>>> getFromDict(dataDict, ["b", "v", "y"])
2
>>> setInDict(dataDict, ["b", "v", "w"], 4)
>>> import pprint
>>> pprint.pprint(dataDict)
{'a': {'r': 1, 's': 2, 't': 3},
 'b': {'u': 1, 'v': {'w': 4, 'x': 1, 'y': 2, 'z': 3}, 'w': 3}}

Tenga en cuenta que la guía de estilo Python PEP8 prescribe nombres snake_case para funciones . Lo anterior funciona igualmente bien para listas o una combinación de diccionarios y listas, por lo que los nombres realmente deberían ser get_by_path()y set_by_path():

from functools import reduce  # forward compatibility for Python 3
import operator

def get_by_path(root, items):
    """Access a nested object in root by item sequence."""
    return reduce(operator.getitem, items, root)

def set_by_path(root, items, value):
    """Set a value in a nested object in root by item sequence."""
    get_by_path(root, items[:-1])[items[-1]] = value
Martijn Pieters
fuente
1
¿Cuánto es confiable este desplazamiento para estructuras anidadas arbitrarias? ¿Funcionará también para diccionarios mixtos con listas anidadas? ¿Cómo modifico getFromDict () para proporcionar default_value y tener default default_value como None? Soy novato en Python con muchos años de desarrollo de PHP y antes del desarrollo de C.
Dmitriy Sintsov
2
También el conjunto mapeado anidado debe crear nodos no existentes, imo: listas para claves enteras, diccionarios para claves de cadena.
Dmitriy Sintsov
1
@ user1353510: como sucede, aquí se usa la sintaxis de indexación regular, por lo que también admitirá listas dentro de los diccionarios. Simplemente pase índices enteros para esos.
Martijn Pieters
1
@ user1353510: para un valor predeterminado, use try:, except (KeyError, IndexError): return default_valuealrededor de la returnlínea actual .
Martijn Pieters
1
@Georgy: el uso dict.get()cambia la semántica, ya que eso devuelve en Nonelugar de aumentar KeyErrorlos nombres faltantes. Cualquier nombre posterior entonces desencadena un AttributeError. operatores una biblioteca estándar, no hay necesidad de evitarla aquí.
Martijn Pieters
40
  1. La solución aceptada no funcionará directamente para python3, necesitará un from functools import reduce.
  2. También parece más pitónico usar un forbucle. Vea la cita de What's New In Python 3.0 .

    Eliminado reduce(). Úselo functools.reduce()si realmente lo necesita; sin embargo, el 99 por ciento de las veces un forciclo explícito es más legible.

  3. A continuación, la solución aceptada no establece claves anidadas no existentes (devuelve a KeyError); consulte la respuesta de @ eafit para obtener una solución

Entonces, ¿por qué no usar el método sugerido de la pregunta de kolergy para obtener un valor?

def getFromDict(dataDict, mapList):    
    for k in mapList: dataDict = dataDict[k]
    return dataDict

Y el código de la respuesta de @eafit para establecer un valor:

def nested_set(dic, keys, value):
    for key in keys[:-1]:
        dic = dic.setdefault(key, {})
    dic[keys[-1]] = value

Ambos trabajan directamente en python 2 y 3

DomTomCat
fuente
66
Prefiero esta solución, pero tenga cuidado. Si no me equivoco, ya que los diccionarios de Python no son inmutables getFromDicttienen el potencial de destruir a la persona que llama dataDict. Yo copy.deepcopy(dataDict)primero. Por supuesto, (como está escrito) este comportamiento se desea en la segunda función.
Dylan F
15

El uso de reducir es inteligente, pero el método de configuración del OP puede tener problemas si las claves principales no existen previamente en el diccionario anidado. Dado que esta es la primera publicación SO que vi para este tema en mi búsqueda de Google, me gustaría mejorarla un poco.

El método set en ( Establecer un valor en un diccionario de Python anidado dada una lista de índices y valores ) parece más robusto para las claves parentales faltantes. Para copiarlo:

def nested_set(dic, keys, value):
    for key in keys[:-1]:
        dic = dic.setdefault(key, {})
    dic[keys[-1]] = value

Además, puede ser conveniente tener un método que atraviese el árbol de claves y obtenga todas las rutas de clave absolutas, para lo cual he creado:

def keysInDict(dataDict, parent=[]):
    if not isinstance(dataDict, dict):
        return [tuple(parent)]
    else:
        return reduce(list.__add__, 
            [keysInDict(v,parent+[k]) for k,v in dataDict.items()], [])

Uno de sus usos es convertir el árbol anidado en un DataFrame de pandas, utilizando el siguiente código (suponiendo que todas las hojas en el diccionario anidado tengan la misma profundidad).

def dict_to_df(dataDict):
    ret = []
    for k in keysInDict(dataDict):
        v = np.array( getFromDict(dataDict, k), )
        v = pd.DataFrame(v)
        v.columns = pd.MultiIndex.from_product(list(k) + [v.columns])
        ret.append(v)
    return reduce(pd.DataFrame.join, ret)
eafit
fuente
¿Por qué limitar arbitrariamente la longitud del argumento 'claves' a 2 o más nested_set?
alancalvitti
10

Esta biblioteca puede ser útil: https://github.com/akesterson/dpath-python

Una biblioteca de Python para acceder y buscar diccionarios a través de / slashed / paths ala xpath

Básicamente, le permite pasar por alto un diccionario como si fuera un sistema de archivos.

dmmfll
fuente
3

¿Qué tal el uso de funciones recursivas?

Para obtener un valor:

def getFromDict(dataDict, maplist):
    first, rest = maplist[0], maplist[1:]

    if rest: 
        # if `rest` is not empty, run the function recursively
        return getFromDict(dataDict[first], rest)
    else:
        return dataDict[first]

Y para establecer un valor:

def setInDict(dataDict, maplist, value):
    first, rest = maplist[0], maplist[1:]

    if rest:
        try:
            if not isinstance(dataDict[first], dict):
                # if the key is not a dict, then make it a dict
                dataDict[first] = {}
        except KeyError:
            # if key doesn't exist, create one
            dataDict[first] = {}

        setInDict(dataDict[first], rest, value)
    else:
        dataDict[first] = value
xyres
fuente
2

Estilo Python puro, sin ninguna importación:

def nested_set(element, value, *keys):
    if type(element) is not dict:
        raise AttributeError('nested_set() expects dict as first argument.')
    if len(keys) < 2:
        raise AttributeError('nested_set() expects at least three arguments, not enough given.')

    _keys = keys[:-1]
    _element = element
    for key in _keys:
        _element = _element[key]
    _element[keys[-1]] = value

example = {"foo": { "bar": { "baz": "ok" } } }
keys = ['foo', 'bar']
nested_set(example, "yay", *keys)
print(example)

Salida

{'foo': {'bar': 'yay'}}
Arount
fuente
2

Una forma alternativa si no desea generar errores si una de las claves está ausente (para que su código principal pueda ejecutarse sin interrupción):

def get_value(self,your_dict,*keys):
    curr_dict_ = your_dict
    for k in keys:
        v = curr_dict.get(k,None)
        if v is None:
            break
        if isinstance(v,dict):
            curr_dict = v
    return v

En este caso, si alguna de las teclas de entrada no está presente, no se devuelve ninguna, que se puede utilizar como un control en su código principal para realizar una tarea alternativa.

Pulkit
fuente
1

En lugar de tener un éxito en el rendimiento cada vez que desea buscar un valor, ¿qué tal si aplana el diccionario una vez y luego simplemente busca la clave como b:v:y

def flatten(mydict):
  new_dict = {}
  for key,value in mydict.items():
    if type(value) == dict:
      _dict = {':'.join([key, _key]):_value for _key, _value in flatten(value).items()}
      new_dict.update(_dict)
    else:
      new_dict[key]=value
  return new_dict

dataDict = {
"a":{
    "r": 1,
    "s": 2,
    "t": 3
    },
"b":{
    "u": 1,
    "v": {
        "x": 1,
        "y": 2,
        "z": 3
    },
    "w": 3
    }
}    

flat_dict = flatten(dataDict)
print flat_dict
{'b:w': 3, 'b:u': 1, 'b:v:y': 2, 'b:v:x': 1, 'b:v:z': 3, 'a:r': 1, 'a:s': 2, 'a:t': 3}

De esta manera, simplemente puede buscar elementos usando lo flat_dict['b:v:y']que le dará 1.

Y en lugar de recorrer el diccionario en cada búsqueda, puede acelerar esto al aplanar el diccionario y guardar la salida para que una búsqueda desde el inicio en frío signifique cargar el diccionario aplanado y simplemente realizar una búsqueda de clave / valor sin el recorrido.

OkezieE
fuente
1

Resuelto esto con recursividad:

def get(d,l):
    if len(l)==1: return d[l[0]]
    return get(d[l[0]],l[1:])

Usando tu ejemplo:

dataDict = {
    "a":{
        "r": 1,
        "s": 2,
        "t": 3
        },
    "b":{
        "u": 1,
        "v": {
            "x": 1,
            "y": 2,
            "z": 3
        },
        "w": 3
        }
}
maplist1 = ["a", "r"]
maplist2 = ["b", "v", "y"]
print(get(dataDict, maplist1)) # 1
print(get(dataDict, maplist2)) # 2
Poh Zi How
fuente
1

¿Qué tal verificar y luego establecer el elemento dict sin procesar todos los índices dos veces?

Solución:

def nested_yield(nested, keys_list):
    """
    Get current nested data by send(None) method. Allows change it to Value by calling send(Value) next time
    :param nested: list or dict of lists or dicts
    :param keys_list: list of indexes/keys
    """
    if not len(keys_list):  # assign to 1st level list
        if isinstance(nested, list):
            while True:
                nested[:] = yield nested
        else:
            raise IndexError('Only lists can take element without key')


    last_key = keys_list.pop()
    for key in keys_list:
        nested = nested[key]

    while True:
        try:
            nested[last_key] = yield nested[last_key]
        except IndexError as e:
            print('no index {} in {}'.format(last_key, nested))
            yield None

Ejemplo de flujo de trabajo:

ny = nested_yield(nested_dict, nested_address)
data_element = ny.send(None)
if data_element:
    # process element
    ...
else:
    # extend/update nested data
    ny.send(new_data_element)
    ...
ny.close()

Prueba

>>> cfg= {'Options': [[1,[0]],[2,[4,[8,16]]],[3,[9]]]}
    ny = nested_yield(cfg, ['Options',1,1,1])
    ny.send(None)
[8, 16]
>>> ny.send('Hello!')
'Hello!'
>>> cfg
{'Options': [[1, [0]], [2, [4, 'Hello!']], [3, [9]]]}
>>> ny.close()
And0k
fuente
1

Muy tarde a la fiesta, pero publicar en caso de que esto pueda ayudar a alguien en el futuro. Para mi caso de uso, la siguiente función funcionó mejor. Funciona para extraer cualquier tipo de datos del diccionario

dict es el diccionario que contiene nuestro valor

lista es una lista de "pasos" hacia nuestro valor

def getnestedvalue(dict, list):

    length = len(list)
    try:
        for depth, key in enumerate(list):
            if depth == length - 1:
                output = dict[key]
                return output
            dict = dict[key]
    except (KeyError, TypeError):
        return None

    return None
Jack Casey
fuente
1

Es satisfactorio ver estas respuestas para tener dos métodos estáticos para establecer y obtener atributos anidados. Estas soluciones son mucho mejores que usar árboles anidados https://gist.github.com/hrldcpr/2012250

Aquí está mi implementación.

Uso :

Para establecer la llamada de atributo anidado sattr(my_dict, 1, 2, 3, 5) is equal to my_dict[1][2][3][4]=5

Para obtener una llamada de atributo anidado gattr(my_dict, 1, 2)

def gattr(d, *attrs):
    """
    This method receives a dict and list of attributes to return the innermost value of the give dict       
    """
    try:
        for at in attrs:
            d = d[at]
        return d
    except(KeyError, TypeError):
        return None


def sattr(d, *attrs):
    """
    Adds "val" to dict in the hierarchy mentioned via *attrs
    For ex:
    sattr(animals, "cat", "leg","fingers", 4) is equivalent to animals["cat"]["leg"]["fingers"]=4
    This method creates necessary objects until it reaches the final depth
    This behaviour is also known as autovivification and plenty of implementation are around
    This implementation addresses the corner case of replacing existing primitives
    https://gist.github.com/hrldcpr/2012250#gistcomment-1779319
    """
    for attr in attrs[:-2]:
        if type(d.get(attr)) is not dict:
            d[attr] = {}
        d = d[attr]
    d[attrs[-2]] = attrs[-1]
nehem
fuente
1

Le sugiero que use python-benedictpara acceder a elementos anidados utilizando keypath.

Instálelo usando pip:

pip install python-benedict

Luego:

from benedict import benedict

dataDict = benedict({
    "a":{
        "r": 1,
        "s": 2,
        "t": 3,
    },
    "b":{
        "u": 1,
        "v": {
            "x": 1,
            "y": 2,
            "z": 3,
        },
        "w": 3,
    },
}) 

print(dataDict['a.r'])
# or
print(dataDict['a', 'r'])

Aquí la documentación completa: https://github.com/fabiocaccamo/python-benedict

Fabio Caccamo
fuente
0

Si también desea la capacidad de trabajar con json arbitrarios, incluidas listas anidadas y dictados, y manejar muy bien las rutas de búsqueda no válidas, esta es mi solución:

from functools import reduce


def get_furthest(s, path):
    '''
    Gets the furthest value along a given key path in a subscriptable structure.

    subscriptable, list -> any
    :param s: the subscriptable structure to examine
    :param path: the lookup path to follow
    :return: a tuple of the value at the furthest valid key, and whether the full path is valid
    '''

    def step_key(acc, key):
        s = acc[0]
        if isinstance(s, str):
            return (s, False)
        try:
            return (s[key], acc[1])
        except LookupError:
            return (s, False)

    return reduce(step_key, path, (s, True))


def get_val(s, path):
    val, successful = get_furthest(s, path)
    if successful:
        return val
    else:
        raise LookupError('Invalid lookup path: {}'.format(path))


def set_val(s, path, value):
    get_val(s, path[:-1])[path[-1]] = value
Grant Palmer
fuente
0

Un método para concatenar cadenas:

def get_sub_object_from_path(dict_name, map_list):
    for i in map_list:
        _string = "['%s']" % i
        dict_name += _string
    value = eval(dict_name)
    return value
#Sample:
_dict = {'new': 'person', 'time': {'for': 'one'}}
map_list = ['time', 'for']
print get_sub_object_from_path("_dict",map_list)
#Output:
#one
lucas
fuente
0

Extendiendo @DomTomCat y el enfoque de otros, estos configuradores y mapeadores funcionales (es decir, devuelven datos modificados mediante copia profunda sin afectar la entrada) funcionan para anidados dicty list.

setter:

def set_at_path(data0, keys, value):
    data = deepcopy(data0)
    if len(keys)>1:
        if isinstance(data,dict):
            return {k:(set_by_path(v,keys[1:],value) if k==keys[0] else v) for k,v in data.items()}
        if isinstance(data,list):
            return [set_by_path(x[1],keys[1:],value) if x[0]==keys[0] else x[1] for x in enumerate(data)]
    else:
        data[keys[-1]]=value
        return data

mapeador:

def map_at_path(data0, keys, f):
    data = deepcopy(data0)
    if len(keys)>1:
        if isinstance(data,dict):
            return {k:(map_at_path(v,keys[1:],f) if k==keys[0] else v) for k,v in data.items()}
        if isinstance(data,list):
            return [map_at_path(x[1],keys[1:],f) if x[0]==keys[0] else x[1] for x in enumerate(data)]
    else:
        data[keys[-1]]=f(data[keys[-1]])
        return data
alancalvitti
fuente
0

Puede hacer uso de la evalfunción en python.

def nested_parse(nest, map_list):
    nestq = "nest['" + "']['".join(map_list) + "']"
    return eval(nestq, {'__builtins__':None}, {'nest':nest})

Explicación

Para su consulta de ejemplo: maplist = ["b", "v", "y"]

nestqserá "nest['b']['v']['y']"dondenest está el diccionario anidado.

La evalfunción incorporada ejecuta la cadena dada. Sin embargo, es importante tener cuidado con las posibles vulnerabilidades que surgen del uso de la evalfunción. La discusión se puede encontrar aquí:

  1. https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html
  2. https://www.journaldev.com/22504/python-eval-function

En la nested_parse()función, me he asegurado de que no haya __builtins__globales disponibles y que solo la variable local que esté disponible sea el nestdiccionario.

Abhirup Das
fuente