Metode aman python untuk mendapatkan nilai kamus bersarang

144

Saya memiliki kamus bersarang. Apakah hanya ada satu cara untuk mendapatkan nilai dengan aman?

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

Atau mungkin python memiliki metode seperti get()untuk kamus bersarang?

Arti
sumber
1
Menurut saya, kode dalam pertanyaan Anda sudah merupakan cara terbaik untuk mengeluarkan nilai bersarang dari kamus. Anda selalu dapat menentukan nilai default di except keyerror:klausa.
Peter Schorn

Jawaban:

280

Anda bisa menggunakan getdua kali:

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

Ini akan kembali Nonejika salah satu key1atau key2tidak ada.

Perhatikan bahwa ini masih bisa menaikkan AttributeErrorjika example_dict['key1']ada tetapi bukan dict (atau objek seperti dict dengan getmetode). The try..exceptkode yang diposting akan menaikkan TypeErrorbukan jika example_dict['key1']adalah unsubscriptable.

Perbedaan lainnya adalah bahwa try...excepthubungan arus pendek segera setelah kunci pertama hilang. Rantai getpanggilan tidak.


Jika Anda ingin mempertahankan sintaks, example_dict['key1']['key2']tetapi tidak ingin itu meningkatkan KeyErrors, maka Anda bisa menggunakan resep 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'>

Perhatikan bahwa ini mengembalikan Hasher kosong ketika kunci hilang.

Karena Hashermerupakan subkelas dari dictAnda dapat menggunakan Hasher dengan cara yang sama seperti Anda dapat menggunakan a dict. Semua metode dan sintaksis yang sama tersedia, Hashers hanya memperlakukan kunci yang hilang secara berbeda.

Anda dapat mengubah biasa dictmenjadi Hasherseperti ini:

hasher = Hasher(example_dict)

dan mengonversi a Hashermenjadi reguler dictdengan mudah:

regular_dict = dict(hasher)

Alternatif lain adalah menyembunyikan keburukan dalam fungsi pembantu:

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

Sehingga sisa kode Anda dapat tetap terbaca:

safeget(example_dict, 'key1', 'key2')
unutbu
sumber
37
jadi, python tidak punya solusi yang bagus untuk kasus ini? :(
Arti
Saya mengalami masalah dengan implementasi yang serupa. Jika Anda memiliki d = {key1: None}, get pertama akan mengembalikan None dan kemudian Anda akan memiliki pengecualian): Saya mencoba mencari solusi untuk ini
Huercio
1
The safegetmetode adalah dalam banyak cara tidak sangat aman karena menimpa kamus asli, yang berarti Anda tidak dapat dengan aman melakukan hal-hal seperti safeget(dct, 'a', 'b') or safeget(dct, 'a').
neverfox
safegetjangan pernah menimpa kamus asli. Ini akan mengembalikan kamus asli, nilai dari kamus asli, atau None.
unutbu
4
@KurtBourbaki: dct = dct[key] menugaskan kembali nilai baru ke variabel lokal dct . Ini tidak mengubah mutasi asli (jadi dict asli tidak terpengaruh oleh safeget.) Jika, di sisi lain, dct[key] = ...telah digunakan, maka dict asli akan diubah. Dengan kata lain, dalam nama Python terikat dengan nilai . Penugasan nilai baru ke nama tidak memengaruhi nilai lama (kecuali jika tidak ada lagi referensi ke nilai lama, dalam hal ini (dalam CPython) akan dikumpulkan sampah.)
unutbu
60

Anda juga bisa menggunakan pengurangan python :

def deep_get(dictionary, *keys):
    return reduce(lambda d, key: d.get(key) if d else None, keys, dictionary)
Yoav T
sumber
5
Hanya ingin menyebutkan bahwa functools tidak lagi merupakan builtin di Python3 dan perlu diimpor dari functools, yang membuat pendekatan ini sedikit kurang elegan.
yoniLavi
3
Koreksi ringan untuk komentar ini: pengurangan tidak lagi menjadi bawaan di Py3. Tapi saya tidak mengerti mengapa ini membuat ini kurang elegan. Ini tidak membuatnya kurang cocok untuk satu-kapal, tetapi menjadi satu-kapal tidak secara otomatis memenuhi syarat atau mendiskualifikasi sesuatu sebagai "elegan".
PaulMcG
30

Dengan menggabungkan semua jawaban ini di sini dan perubahan kecil yang saya buat, saya pikir fungsi ini akan berguna. ini aman, cepat, mudah dirawat.

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)

Contoh:

>>> 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
sumber
1
Sempurna untuk templat Jinja2
Thomas
Ini adalah solusi yang baik meskipun ada juga kerugiannya: bahkan jika kunci pertama tidak tersedia, atau nilai yang diteruskan karena argumen kamus ke fungsi bukan kamus, fungsi akan beralih dari elemen pertama ke yang terakhir. Pada dasarnya, ia melakukan ini dalam semua kasus.
Arseny
1
deep_get({'a': 1}, "a.b")memberi Nonetetapi saya akan mengharapkan pengecualian seperti KeyErroratau sesuatu yang lain.
stackunderflow
@edityouprofile. maka Anda hanya perlu melakukan modifikasi kecil untuk mengubah nilai kembali dari NonekeRaise KeyError
Yuda Prawira
15

Membangun jawaban Yoav, pendekatan yang bahkan lebih aman:

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

Solusi rekursif. Ini bukan yang paling efisien tetapi saya merasa sedikit lebih mudah dibaca daripada contoh lain dan tidak bergantung pada functools.

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

Contoh

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

Versi yang lebih halus

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

Sementara pendekatan pengurangan rapi dan pendek, saya pikir loop sederhana lebih mudah untuk grok. Saya juga menyertakan parameter default.

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

Sebagai latihan untuk memahami bagaimana mengurangi satu-liner bekerja, saya melakukan hal berikut. Tetapi pada akhirnya pendekatan loop tampaknya lebih intuitif bagi saya.

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)

Pemakaian

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

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

Saya sarankan Anda untuk mencoba python-benedict.

Ini adalah dictsubclass yang menyediakan dukungan keypath dan banyak lagi.

Instalasi: pip install python-benedict

from benedict import benedict

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

sekarang Anda dapat mengakses nilai bersarang menggunakan keypath :

val = example_dict['key1.key2']

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

atau akses nilai bersarang menggunakan daftar kunci :

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

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

Ini diuji dengan baik dan open-source di GitHub :

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

Fabio Caccamo
sumber
@ perfecto25 terima kasih! Saya akan segera merilis fitur baru, tetap disini tun
Fabio Caccamo
@ perfecto25 Saya menambahkan dukungan ke daftar indeks, mis. d.get('a.b[0].c[-1]')
Fabio Caccamo
4

Kelas sederhana yang dapat membungkus dict, dan mengambil berdasarkan kunci:

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

Sebagai contoh:

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

Jika kunci tidak ada, itu kembali Nonesecara default. Anda dapat mengesampingkannya menggunakan default=kunci dalam FindDictpembungkus - misalnya`:

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

untuk pengambilan kunci tingkat kedua, Anda dapat melakukan ini:

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

Setelah melihat ini untuk mendapatkan atribut mendalam, saya membuat yang berikut untuk mendapatkan dictnilai tersarang dengan aman menggunakan notasi titik. Ini berfungsi untuk saya karena dictsobjek MongoDB saya deserialisasi, jadi saya tahu nama kunci tidak mengandung .s. Juga, dalam konteks saya, saya bisa menentukan nilai fallback palsu ( None) yang tidak saya miliki di data saya, jadi saya bisa menghindari pola coba / kecuali saat memanggil fungsi.

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
sumber
7
fallbacksebenarnya tidak digunakan dalam fungsi.
153957
Perhatikan bahwa ini tidak berfungsi untuk kunci yang berisi.
JW.
Ketika kami memanggil obj [nama] mengapa tidak obj.get (nama, fallback) dan hindari try-catch (jika Anda menginginkan try-catch, maka kembalikan fallback, bukan None)
denvar
Terima kasih @ 153957. Aku telah memperbaikinya. Dan ya @ JP, ini berfungsi untuk kasus penggunaan saya. Anda dapat menambahkan sep=','argumen kata kunci untuk menggeneralisasi untuk kondisi tertentu (sep, mundur). Dan @denvar, jika objmengatakan tipe intsetelah urutan pengurangan, maka obj [nama] memunculkan TypeError, yang saya tangkap. Jika saya menggunakan obj.get (nama) atau obj.get (nama, fallback) sebagai gantinya, itu akan meningkatkan AttributeError, jadi saya harus menangkapnya.
Donny Winston
1

Namun fungsi lain untuk hal yang sama, juga mengembalikan boolean untuk mewakili apakah kunci itu ditemukan atau tidak dan menangani beberapa kesalahan yang tidak terduga.

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

contoh penggunaan:

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

(Benar, 'holla')

(Salah, '')

penduDev
sumber
0

Adaptasi jawaban unutbu yang menurut saya berguna dalam kode saya sendiri:

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

Ini menghasilkan entri kamus untuk key1 jika belum memiliki kunci itu sehingga Anda menghindari KeyError. Jika Anda ingin membuat kamus bersarang yang menyertakan pasangan kunci itu seperti yang saya lakukan, ini sepertinya solusi termudah.

GenesRus
sumber
0

Karena meningkatkan kesalahan kunci jika salah satu kunci tidak ada adalah hal yang wajar untuk dilakukan, kita bahkan tidak dapat memeriksanya dan membuatnya tunggal seperti itu:

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

Sedikit perbaikan pada reducependekatan untuk membuatnya bekerja dengan daftar. Juga menggunakan jalur data sebagai string yang dibagi dengan titik, bukan array.

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
sumber
0

Sebuah solusi yang saya gunakan yang mirip dengan get ganda tetapi dengan kemampuan tambahan untuk menghindari TypeError menggunakan logika if else:

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

Namun, semakin bersarang kamus semakin rumit ini menjadi.

Adam The Housman
sumber
0

Untuk pencarian kamus / pencarian JSON, Anda dapat menggunakan diktor

pip instal dictor

objek dikt

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

untuk mendapatkan item Lonestar, cukup sediakan jalur terpisah-titik, yaitu

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

Anda dapat memberikan nilai cadangan jika kuncinya tidak ada di jalur

ada banyak opsi yang dapat Anda lakukan, seperti abaikan casing huruf dan menggunakan karakter lain selain '.' sebagai pemisah jalur,

https://github.com/perfecto25/dictor

perfecto25
sumber
0

Saya sedikit mengubah jawaban ini . Saya menambahkan memeriksa apakah kita menggunakan daftar dengan angka. Jadi sekarang kita bisa menggunakannya dengan cara apa pun. deep_get(allTemp, [0], {})atau deep_get(getMinimalTemp, [0, minimalTemperatureKey], 26)lainnya

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)
Berani1991
sumber
0

Sudah ada banyak jawaban bagus tapi saya telah datang dengan fungsi yang disebut get mirip dengan lodash dapatkan di JavaScript yang juga mendukung mencapai ke daftar dengan indeks:

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
sumber