Zugriff auf verschachtelte Wörterbuchelemente über eine Liste von Schlüsseln?


143

Ich habe eine komplexe Wörterbuchstruktur, auf die ich über eine Liste von Schlüsseln zugreifen möchte, um das richtige Element zu adressieren.

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

maplist = ["a", "r"]

oder

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

Ich habe den folgenden Code erstellt, der funktioniert, aber ich bin sicher, dass es einen besseren und effizienteren Weg gibt, dies zu tun, wenn jemand eine Idee hat.

# 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

Antworten:


230

Verwenden Sie reduce()diese Option, um das Wörterbuch zu durchlaufen:

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

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

und wiederverwenden getFromDict, um den Speicherort für den Wert zu finden für setInDict():

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

Alle bis auf das letzte Element in mapListwerden benötigt, um das 'übergeordnete' Wörterbuch zu finden, zu dem der Wert hinzugefügt werden soll. Verwenden Sie dann das letzte Element, um den Wert auf den richtigen Schlüssel zu setzen.

Demo:

>>> 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}}

Beachten Sie, dass der Python PEP8- Styleguide snake_case-Namen für Funktionen vorschreibt . Das Obige funktioniert genauso gut für Listen oder eine Mischung aus Wörterbüchern und Listen, daher sollten die Namen wirklich sein get_by_path()und 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

1
Inwieweit ist ein solches Durchlaufen für beliebig verschachtelte Strukturen zuverlässig? Funktioniert es auch für gemischte Wörterbücher mit verschachtelten Listen? Wie ändere ich getFromDict (), um default_value anzugeben und default default_value als None zu haben? Ich bin ein Anfänger in Python mit langjähriger PHP-Entwicklung und vor der C-Entwicklung.
Dmitriy Sintsov

2
Auch verschachtelte zugeordnete Mengen sollten nicht vorhandene Knoten erstellen, imo: Listen für Ganzzahlschlüssel, Wörterbücher für Zeichenfolgenschlüssel.
Dmitriy Sintsov

1
@ user1353510: Hier wird zufällig die reguläre Indizierungssyntax verwendet, sodass auch Listen in Wörterbüchern unterstützt werden. Übergeben Sie einfach ganzzahlige Indizes für diese.
Martijn Pieters

1
@ user1353510: für einen Standardwert, verwenden try:, except (KeyError, IndexError): return default_valueum die aktuelle returnZeile.
Martijn Pieters

1
@Georgy: Durch die Verwendung von dict.get()Änderungen wird die Semantik geändert, da dies bei fehlenden Namen Noneeher zurückkehrt als erhöht KeyError. Alle nachfolgenden Namen lösen dann eine aus AttributeError. operatorist eine Standardbibliothek, die hier nicht vermieden werden muss.
Martijn Pieters

40
  1. Die akzeptierte Lösung funktioniert nicht direkt für Python3 - sie benötigt eine from functools import reduce.
  2. Es scheint auch pythonischer, eine forSchleife zu verwenden. Siehe das Zitat aus den Neuerungen in Python 3.0 .

    Entfernt reduce(). Verwenden functools.reduce()Sie, wenn Sie es wirklich brauchen; In 99 Prozent der forFälle ist eine explizite Schleife jedoch besser lesbar.

  3. Als Nächstes setzt die akzeptierte Lösung keine nicht vorhandenen verschachtelten Schlüssel (sie gibt a zurück KeyError) - eine Lösung finden Sie in der Antwort von @ eafit

Warum also nicht die vorgeschlagene Methode aus Kolergys Frage verwenden, um einen Wert zu erhalten:

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

Und der Code aus der Antwort von @ eafit zum Festlegen eines Werts:

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

Beide arbeiten direkt in Python 2 und 3


6
Ich bevorzuge diese Lösung - aber sei vorsichtig. Wenn ich mich nicht irre, da Python-Wörterbücher nicht unveränderlich getFromDictsind, kann dies den Anrufer zerstören dataDict. Ich würde copy.deepcopy(dataDict)zuerst. Natürlich ist (wie geschrieben) dieses Verhalten in der zweiten Funktion erwünscht.
Dylan F

15

Die Verwendung von "Reduzieren" ist clever, aber die Set-Methode des OP kann Probleme haben, wenn die übergeordneten Schlüssel nicht im verschachtelten Wörterbuch vorhanden sind. Da dies der erste SO-Beitrag ist, den ich in meiner Google-Suche für dieses Thema gesehen habe, möchte ich ihn etwas verbessern.

Die set-Methode in ( Festlegen eines Werts in einem verschachtelten Python-Wörterbuch anhand einer Liste von Indizes und Werten ) scheint gegenüber fehlenden Elternschlüsseln robuster zu sein. So kopieren Sie es:

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

Es kann auch praktisch sein, eine Methode zu haben, die den Schlüsselbaum durchläuft und alle absoluten Schlüsselpfade abruft, für die ich Folgendes erstellt habe:

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()], [])

Eine Verwendung davon besteht darin, den verschachtelten Baum unter Verwendung des folgenden Codes in einen Pandas-DataFrame zu konvertieren (vorausgesetzt, alle Blätter im verschachtelten Wörterbuch haben dieselbe Tiefe).

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)

Warum willkürlich die Argumentlänge der 'Schlüssel' auf 2 oder mehr in begrenzen nested_set?
Alancalvitti

10

Diese Bibliothek kann hilfreich sein: https://github.com/akesterson/dpath-python

Eine Python-Bibliothek für den Zugriff auf und das Durchsuchen von Wörterbüchern über / slashed / path ala xpath

Grundsätzlich können Sie ein Wörterbuch so durchsuchen, als wäre es ein Dateisystem.


3

Wie wäre es mit rekursiven Funktionen?

So erhalten Sie einen Wert:

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]

Und um einen Wert festzulegen:

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

2

Reiner Python-Stil ohne Import:

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)

Ausgabe

{'foo': {'bar': 'yay'}}

2

Eine alternative Möglichkeit, wenn Sie keine Fehler auslösen möchten, wenn einer der Schlüssel fehlt (damit Ihr Hauptcode ohne Unterbrechung ausgeführt werden kann):

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

In diesem Fall wird None zurückgegeben, wenn einer der Eingabetasten nicht vorhanden ist. Dies kann als Überprüfung Ihres Hauptcodes verwendet werden, um eine alternative Aufgabe auszuführen.


1

Anstatt jedes Mal einen Leistungstreffer zu erzielen, wenn Sie einen Wert nachschlagen möchten, können Sie das Wörterbuch einmal reduzieren und dann einfach den Schlüssel wie nachschlagen 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}

Auf diese Weise können Sie einfach nach Artikeln suchen, mit flat_dict['b:v:y']denen Sie diese erhalten 1.

Und anstatt das Wörterbuch bei jeder Suche zu durchlaufen, können Sie dies möglicherweise beschleunigen, indem Sie das Wörterbuch reduzieren und die Ausgabe speichern, sodass eine Suche nach Kaltstart das Laden des reduzierten Wörterbuchs und das einfache Durchführen einer Schlüssel- / Wertsuche mit Nein bedeutet Durchquerung.


1

Dieses Problem wurde durch Rekursion gelöst:

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

Anhand Ihres Beispiels:

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

1

Wie wäre es, wenn Sie das Element dict überprüfen und dann festlegen, ohne alle Indizes zweimal zu verarbeiten?

Lösung:

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

Beispielworkflow:

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()

Prüfung

>>> 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()

1

Sehr spät zur Party, aber das Posten für den Fall, dass dies jemandem in der Zukunft helfen könnte. Für meinen Anwendungsfall hat die folgende Funktion am besten funktioniert. Funktioniert, um einen beliebigen Datentyp aus dem Wörterbuch zu ziehen

dict ist das Wörterbuch, das unseren Wert enthält

Liste ist eine Liste von "Schritten" in Richtung unseres Wertes

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

1

Es ist erfreulich, diese Antworten zu sehen, wenn zwei statische Methoden zum Festlegen und Abrufen verschachtelter Attribute vorhanden sind. Diese Lösungen sind weitaus besser als die Verwendung verschachtelter Bäume https://gist.github.com/hrldcpr/2012250

Hier ist meine Implementierung.

Verwendung :

Aufruf eines verschachtelten Attributs festlegen sattr(my_dict, 1, 2, 3, 5) is equal to my_dict[1][2][3][4]=5

Um einen verschachtelten Attributaufruf zu erhalten 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]

1

Ich empfehle Ihnen, python-benedictüber den Schlüsselpfad auf verschachtelte Elemente zuzugreifen.

Installieren Sie es mit pip:

pip install python-benedict

Dann:

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'])

Hier die vollständige Dokumentation: https://github.com/fabiocaccamo/python-benedict


0

Wenn Sie auch die Möglichkeit haben möchten, mit beliebigem JSON einschließlich verschachtelter Listen und Diktate zu arbeiten und ungültige Suchpfade gut zu handhaben, ist hier meine Lösung:

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

0

eine Methode zum Verketten von Zeichenfolgen:

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

0

Diese funktionalen Setter und Mapper erweitern @DomTomCat und den Ansatz anderer und funktionieren für verschachtelte dictund (dh modifizierte Daten über Deepcopy zurückgeben, ohne die Eingabe zu beeinflussen) 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

Mapper:

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

0

Sie können die evalFunktion in Python verwenden.

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

Erläuterung

Für Ihre Beispielabfrage: maplist = ["b", "v", "y"]

nestqwird sein, "nest['b']['v']['y']"wo nestist das verschachtelte Wörterbuch.

Die evaleingebaute Funktion führt die angegebene Zeichenfolge aus. Es ist jedoch wichtig, vorsichtig mit möglichen Schwachstellen umzugehen, die sich aus der Verwendung von evalFunktionen ergeben. Die Diskussion finden Sie hier:

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

In der nested_parse()Funktion habe ich sichergestellt, dass keine __builtins__Globals verfügbar sind und nur die verfügbare lokale Variable das nestWörterbuch ist.


Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.