Python-sichere Methode zum Abrufen des Werts des verschachtelten Wörterbuchs

143

Ich habe ein verschachteltes Wörterbuch. Gibt es nur einen Weg, um Werte sicher herauszuholen?

try:
    example_dict['key1']['key2']
except KeyError:
    pass

Oder hat Python eine Methode wie get()für verschachtelte Wörterbücher?

Arti
quelle
1
Der Code in Ihrer Frage ist meiner Ansicht nach bereits der beste Weg, um verschachtelte Werte aus dem Wörterbuch zu entfernen. Sie können in der except keyerror:Klausel jederzeit einen Standardwert angeben .
Peter Schorn

Antworten:

278

Sie könnten getzweimal verwenden:

example_dict.get('key1', {}).get('key2')

Dies wird zurückgegeben, Nonewenn eines vorhanden ist key1oder key2nicht.

Beachten Sie, dass dies immer noch ein AttributeErrorif example_dict['key1']auslösen kann, aber kein Diktat (oder ein diktatähnliches Objekt mit einer getMethode) ist. Der try..exceptCode, den Sie gepostet haben, würde TypeErrorstattdessen ein if auslösenexample_dict['key1'] abonnierbar ist.

Ein weiterer Unterschied besteht darin, dass die try...exceptKurzschlüsse unmittelbar nach dem ersten fehlenden Schlüssel erfolgen. Die Anrufkette getnicht.


Wenn Sie die Syntax beibehalten möchten, example_dict['key1']['key2']aber nicht möchten, dass KeyErrors jemals ausgelöst wird, können Sie das Hasher-Rezept verwenden :

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

Beachten Sie, dass dies einen leeren Hasher zurückgibt, wenn ein Schlüssel fehlt.

Da Hasheres sich um eine Unterklasse von dictIhnen handelt, können Sie einen Hasher genauso verwenden wie einen dict. Es stehen dieselben Methoden und Syntax zur Verfügung. Hashers behandeln fehlende Schlüssel nur unterschiedlich.

Sie können eine reguläre dictin eine Hashersolche umwandeln :

hasher = Hasher(example_dict)

und konvertieren Sie ein Hasherin ein reguläres dictgenauso einfach:

regular_dict = dict(hasher)

Eine andere Alternative besteht darin, die Hässlichkeit in einer Hilfsfunktion zu verbergen:

def safeget(dct, *keys):
    for key in keys:
        try:
            dct = dct[key]
        except KeyError:
            return None
    return dct

Der Rest Ihres Codes kann also relativ lesbar bleiben:

safeget(example_dict, 'key1', 'key2')
unutbu
quelle
35
Arti
Ich bin auf ein Problem mit einer ähnlichen Implementierung gestoßen. Wenn Sie d = {key1: None} haben, gibt der erste get None zurück und dann haben Sie eine Ausnahme): Ich versuche, eine Lösung dafür zu finden
Huercio
1
Die safegetMethode ist in vielerlei Hinsicht nicht sehr sicher, da sie das ursprüngliche Wörterbuch überschreibt, was bedeutet, dass Sie solche Dinge nicht sicher tun können safeget(dct, 'a', 'b') or safeget(dct, 'a').
Neverfox
safegetüberschreibt niemals das ursprüngliche Wörterbuch. Es wird entweder das ursprüngliche Wörterbuch, ein Wert aus dem ursprünglichen Wörterbuch oder zurückgegeben None.
Unutbu
4
@KurtBourbaki: dct = dct[key] neu zuweist einen neuen Wert der lokalen Variablen dct . Dies mutiert nicht das ursprüngliche Diktat (daher bleibt das ursprüngliche Diktat davon unberührt safeget). Wenn andererseits verwendet dct[key] = ...worden wäre, wäre das ursprüngliche Diktat geändert worden. Mit anderen Worten, in Python sind Namen an Werte gebunden . Die Zuweisung eines neuen Werts zu einem Namen wirkt sich nicht auf den alten Wert aus (es sei denn, es gibt keine Verweise mehr auf den alten Wert. In diesem Fall (in CPython) wird der Müll gesammelt.)
Unutbu
60

Sie können auch Python Reduce verwenden :

def deep_get(dictionary, *keys):
    return reduce(lambda d, key: d.get(key) if d else None, keys, dictionary)
Yoav T.
quelle
5
Ich wollte nur erwähnen, dass functools nicht mehr in Python3 integriert ist und aus functools importiert werden muss, was diesen Ansatz etwas weniger elegant macht.
YoniLavi
3
Leichte Korrektur dieses Kommentars: Reduzieren ist in Py3 nicht mehr integriert. Aber ich verstehe nicht, warum dies weniger elegant ist. Es macht es weniger geeignet für einen Einzeiler, aber ein Einzeiler zu sein, qualifiziert oder disqualifiziert etwas nicht automatisch als "elegant".
PaulMcG
30

Durch die Kombination all dieser Antworten hier und kleiner Änderungen, die ich vorgenommen habe, halte ich diese Funktion für nützlich. Es ist sicher, schnell und leicht zu warten.

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)

Beispiel:

>>> 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
>>>
Yuda Prawira
quelle
1
Perfekt für Jinja2-Vorlagen
Thomas
Dies ist eine gute Lösung, hat aber auch einen Nachteil: Selbst wenn der erste Schlüssel nicht verfügbar ist oder der als Wörterbuchargument an die Funktion übergebene Wert kein Wörterbuch ist, wechselt die Funktion vom ersten zum letzten Element. Grundsätzlich ist dies in allen Fällen der Fall.
Arseny
1
deep_get({'a': 1}, "a.b")gibt Noneaber ich würde eine ausnahme wie KeyErroroder etwas anderes erwarten .
Stackunderflow
@edityouprofile. dann müssen Sie nur noch kleine Änderungen NoneRaise KeyError
vornehmen
15

Aufbauend auf Yoavs Antwort ein noch sicherer Ansatz:

def deep_get(dictionary, *keys):
    return reduce(lambda d, key: d.get(key, None) if isinstance(d, dict) else None, keys, dictionary)
Jose Alban
quelle
12

Eine rekursive Lösung. Es ist nicht das effizienteste, aber ich finde es etwas lesbarer als die anderen Beispiele und es basiert nicht auf functools.

def deep_get(d, keys):
    if not keys or d is None:
        return d
    return deep_get(d.get(keys[0]), keys[1:])

Beispiel

d = {'meta': {'status': 'OK', 'status_code': 200}}
deep_get(d, ['meta', 'status_code'])     # => 200
deep_get(d, ['garbage', 'status_code'])  # => None

Eine poliertere Version

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)
Pithikos
quelle
7

Während der Reduktionsansatz ordentlich und kurz ist, denke ich, dass eine einfache Schleife leichter zu groken ist. Ich habe auch einen Standardparameter eingefügt.

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

Um zu verstehen, wie der reduzierte Einzeiler funktioniert, habe ich Folgendes getan. Aber letztendlich scheint mir der Loop-Ansatz intuitiver zu sein.

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)

Verwendung

nested = {'a': {'b': {'c': 42}}}

print deep_get(nested, ['a', 'b'])
print deep_get(nested, ['a', 'b', 'z', 'z'], default='missing')
zzz
quelle
5

Ich schlage vor, Sie versuchen es python-benedict .

Es ist ein dict Unterklasse, die Tastaturpfadunterstützung und vieles mehr bietet.

Installation: pip install python-benedict

from benedict import benedict

example_dict = benedict(example_dict, keypath_separator='.')

Jetzt können Sie über den Schlüsselpfad auf verschachtelte Werte zugreifen :

val = example_dict['key1.key2']

# using 'get' method to avoid a possible KeyError:
val = example_dict.get('key1.key2')

oder greifen Sie über die Schlüsselliste auf verschachtelte Werte zu :

val = example_dict['key1', 'key2']

# using get to avoid a possible KeyError:
val = example_dict.get(['key1', 'key2'])

Es ist gut getestet und Open Source auf GitHub :

https://github.com/fabiocaccamo/python-benedict

Fabio Caccamo
quelle
@ perfecto25 danke! Ich werde bald neue Funktionen veröffentlichen, bleibt dran 😉
Fabio Caccamo
@ perfecto25 Ich habe Unterstützung für Listenindizes hinzugefügt, z. d.get('a.b[0].c[-1]')
Fabio Caccamo
4

Eine einfache Klasse, die ein Diktat umschließen und basierend auf einem Schlüssel abrufen kann:

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

Beispielsweise:

person = {'person':{'name':{'first':'John'}}}
FindDict(person).get('person.name.first') # == 'John'

Wenn der Schlüssel nicht vorhanden ist, wird er Nonestandardmäßig zurückgegeben. Sie können dies mit einem default=Schlüssel im FindDictWrapper überschreiben - zum Beispiel`:

FindDict(person, default='').get('person.name.last') # == doesn't exist, so ''
Lee Benson
quelle
3

Für das Abrufen von Schlüsseln der zweiten Ebene können Sie Folgendes tun:

key2_value = (example_dict.get('key1') or {}).get('key2')
Jacob CUI
quelle
2

Nachdem ich dies gesehen hatte , um Attribute gründlich abzurufen, habe ich Folgendes ausgeführt, um verschachtelte dictWerte mithilfe der Punktnotation sicher abzurufen . Dies funktioniert bei mir, da meine dictsObjekte deserialisiert sind und ich weiß, dass die Schlüsselnamen keine .s enthalten . Außerdem kann ich in meinem Kontext einen falschen Fallback-Wert ( None) angeben , den ich nicht in meinen Daten habe, damit ich beim Aufrufen der Funktion das Muster try / exception vermeiden kann.

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)
Donny Winston
quelle
7
fallbackwird in der Funktion nicht verwendet.
153957
Beachten Sie, dass dies nicht für Schlüssel funktioniert, die ein.
JW enthalten.
Wenn wir obj [Name] aufrufen, warum nicht obj.get (Name, Fallback) und den Try-Catch vermeiden (wenn Sie den Try-Catch wollen, dann geben Sie Fallback zurück, nicht None)
denvar
Danke @ 153957. Ich habe es repariert. Und ja @JW, das funktioniert für meinen Anwendungsfall. Sie können ein sep=','Schlüsselwort arg hinzufügen, um es für bestimmte Bedingungen (Sep, Fallback) zu verallgemeinern. Und @denvar, wenn nach einer Sequenz des Reduzierens objvom Typ gesprochen wird int, dann löst obj [name] einen TypeError aus, den ich abfange. Wenn ich stattdessen obj.get (Name) oder obj.get (Name, Fallback) verwenden würde, würde dies einen AttributeError auslösen, so oder so müsste ich abfangen.
Donny Winston
1

Eine weitere Funktion für dasselbe gibt ebenfalls einen Booleschen Wert zurück, um darzustellen, ob der Schlüssel gefunden wurde oder nicht, und behandelt einige unerwartete Fehler.

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

Anwendungsbeispiel:

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

(Stimmt, 'holla')

(Falsch, '')

penduDev
quelle
0

Eine Anpassung der Antwort von unutbu, die ich in meinem eigenen Code nützlich fand:

example_dict.setdefaut('key1', {}).get('key2')

Es generiert einen Wörterbucheintrag für key1, wenn dieser Schlüssel noch nicht vorhanden ist, sodass Sie den KeyError vermeiden. Wenn Sie ein verschachteltes Wörterbuch erstellen möchten, das diese Schlüsselpaarung sowieso wie ich enthält, scheint dies die einfachste Lösung zu sein.

GenesRus
quelle
0

Da es vernünftig ist, einen Schlüsselfehler auszulösen, wenn einer der Schlüssel fehlt, können wir ihn nicht einmal überprüfen und so einfach erhalten:

def get_dict(d, kl):
  cur = d[kl[0]]
  return get_dict(cur, kl[1:]) if len(kl) > 1 else cur
Ben Usman
quelle
0

Wenig Verbesserung zu reducenähern, damit es mit Liste funktioniert. Verwenden Sie auch den Datenpfad als Zeichenfolge, die durch Punkte anstelle des Arrays geteilt wird.

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)
Mike Kor
quelle
0

Eine Lösung, die ich verwendet habe und die dem Double Get ähnelt, aber zusätzlich die Möglichkeit bietet, einen TypeError mithilfe der if else-Logik zu vermeiden:

    value = example_dict['key1']['key2'] if example_dict.get('key1') and example_dict['key1'].get('key2') else default_value

Je verschachtelter das Wörterbuch ist, desto umständlicher wird dies.

Adam der Hausmann
quelle
0

Für verschachtelte Wörterbuch- / JSON-Suchvorgänge können Sie dictor verwenden

pip install dictor

Objekt diktieren

{
    "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"
            ]
        }
    }
}

Um Lonestars Gegenstände zu erhalten, geben Sie einfach einen durch Punkte getrennten Pfad an, d. h

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

Sie können einen Fallback-Wert angeben, falls sich der Schlüssel nicht im Pfad befindet

Es gibt unzählige weitere Optionen, die Sie tun können, z. B. das Groß- und Kleinschreibung von Buchstaben ignorieren und andere Zeichen als '.' als Pfadtrennzeichen

https://github.com/perfecto25/dictor

perfecto25
quelle
0

Ich habe diese Antwort wenig geändert . Ich habe hinzugefügt, um zu überprüfen, ob wir eine Liste mit Zahlen verwenden. Jetzt können wir es also auf jede Art und Weise verwenden. deep_get(allTemp, [0], {})oder 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)
Darex1991
quelle
0

Es gibt bereits viele gute Antworten, aber ich habe eine Funktion namens get ähnlich wie lodash get in JavaScript land entwickelt, die auch das Aufrufen von Listen nach Index unterstützt:

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
Peter Marklund
quelle