Mengakses item kamus bersarang melalui daftar kunci?

143

Saya memiliki struktur kamus yang kompleks yang ingin saya akses melalui daftar kunci untuk membahas item yang benar.

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

maplist = ["a", "r"]

atau

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

Saya telah membuat kode berikut yang berfungsi tetapi saya yakin ada cara yang lebih baik dan lebih efisien untuk melakukan ini jika ada yang punya ide.

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

Jawaban:

230

Gunakan reduce()untuk melintasi kamus:

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

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

dan gunakan kembali getFromDictuntuk menemukan lokasi untuk menyimpan nilai untuk setInDict():

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

Semua kecuali elemen terakhir mapListdiperlukan untuk menemukan kamus 'induk' untuk menambahkan nilai, lalu gunakan elemen terakhir untuk mengatur nilai ke tombol kanan.

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

Perhatikan bahwa panduan gaya Python PEP8 menentukan nama fungsi snake_case . Di atas berfungsi sama baiknya untuk daftar atau campuran kamus dan daftar, sehingga nama harus benar-benar get_by_path()dan 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
sumber
1
Berapa banyak melintasi seperti itu dapat diandalkan untuk struktur bersarang sewenang-wenang? Apakah ini akan berfungsi untuk kamus campuran dengan daftar bersarang juga? Bagaimana cara mengubah getFromDict () untuk memberikan nilai default_value dan nilai default_value default sebagai Tidak Ada? Saya pemula di Python dengan bertahun-tahun pengembangan PHP dan sebelum pengembangan C.
Dmitriy Sintsov
2
Set yang dipetakan bersarang juga harus membuat node yang tidak ada, imo: daftar untuk kunci integer, kamus untuk kunci string.
Dmitriy Sintsov
1
@ user1353510: seperti yang terjadi, sintaks pengindeksan biasa digunakan di sini, jadi itu akan mendukung daftar di dalam kamus juga. Cukup berikan indeks integer untuk itu.
Martijn Pieters
1
@ user1353510: untuk nilai default, gunakan try:, di except (KeyError, IndexError): return default_valuesekitar returnbaris saat ini .
Martijn Pieters
1
@ Georgy: menggunakan dict.get()perubahan semantik, karena itu mengembalikan Nonedaripada menaikkan KeyErroruntuk nama yang hilang. Setiap nama berikutnya kemudian memicu AttributeError. operatoradalah perpustakaan standar, tidak perlu menghindarinya di sini.
Martijn Pieters
40
  1. Solusi yang diterima tidak akan bekerja secara langsung untuk python3 - ini akan membutuhkan from functools import reduce.
  2. Juga tampaknya lebih pythonic untuk menggunakan forloop. Lihat kutipan dari What's New In Python 3.0 .

    Dihapus reduce(). Gunakan functools.reduce()jika Anda benar-benar membutuhkannya; namun, 99 persen dari waktu forperulangan eksplisit lebih mudah dibaca.

  3. Selanjutnya, solusi yang diterima tidak menetapkan kunci bertingkat yang tidak ada (mengembalikan a KeyError) - lihat jawaban @ eafit untuk solusi

Jadi mengapa tidak menggunakan metode yang disarankan dari pertanyaan kolergy untuk mendapatkan nilai:

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

Dan kode dari jawaban @ eafit untuk menetapkan nilai:

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

Keduanya bekerja dengan lurus di python 2 dan 3

DomTomCat
sumber
6
Saya lebih suka solusi ini - tapi hati-hati. Jika saya tidak salah, karena kamus Python tidak berubah getFromDictmemiliki potensi untuk menghancurkan penelepon dataDict. Saya akan copy.deepcopy(dataDict)terlebih dahulu. Tentu saja, (seperti yang tertulis) perilaku ini diinginkan dalam fungsi kedua.
Dylan F
15

Menggunakan pengurangan itu pintar, tetapi metode set OP mungkin memiliki masalah jika kunci induk tidak ada sebelumnya dalam kamus bersarang. Karena ini adalah posting SO pertama yang saya lihat untuk subjek ini di pencarian google saya, saya ingin membuatnya sedikit lebih baik.

Metode set di ( Menetapkan nilai dalam kamus python bersarang diberikan daftar indeks dan nilai ) tampaknya lebih kuat untuk kehilangan kunci orangtua. Untuk menyalinnya:

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

Juga, akan lebih mudah untuk memiliki metode yang melintasi pohon kunci dan mendapatkan semua jalur kunci absolut, yang telah saya buat:

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

Salah satu penggunaannya adalah untuk mengkonversi pohon bersarang ke panda DataFrame, menggunakan kode berikut (dengan asumsi bahwa semua daun dalam kamus bersarang memiliki kedalaman yang sama).

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
sumber
mengapa sewenang-wenang membatasi panjang argumen 'kunci' menjadi 2 atau lebih nested_set?
alancalvitti
10

Perpustakaan ini mungkin bermanfaat: https://github.com/akesterson/dpath-python

Pustaka python untuk mengakses dan mencari kamus melalui / slash / path ala xpath

Pada dasarnya itu memungkinkan Anda menggumpal kamus seolah-olah itu adalah sistem file.

dmmfll
sumber
3

Bagaimana kalau menggunakan fungsi rekursif?

Untuk mendapatkan nilai:

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]

Dan untuk menetapkan nilai:

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

Gaya Python murni, tanpa impor apa pun:

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)

Keluaran

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

Cara alternatif jika Anda tidak ingin meningkatkan kesalahan jika salah satu kunci tidak ada (sehingga kode utama Anda dapat berjalan tanpa gangguan):

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

Dalam hal ini, jika salah satu kunci input tidak ada, Tidak ada yang dikembalikan, yang dapat digunakan sebagai tanda centang pada kode utama Anda untuk melakukan tugas alternatif.

Pulkit
sumber
1

Alih-alih mengambil hit kinerja setiap kali Anda ingin mencari nilai, bagaimana kalau Anda meratakan kamus sekali kemudian cukup mencari kunci seperti 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}

Dengan cara ini Anda bisa mencari barang flat_dict['b:v:y']yang akan digunakan 1.

Dan alih-alih melintasi kamus pada setiap pencarian, Anda mungkin dapat mempercepat ini dengan meratakan kamus dan menyimpan output sehingga pencarian dari awal akan berarti memuat kamus yang diratakan dan hanya melakukan pencarian kunci / nilai tanpa traversal.

OkezieE
sumber
1

Memecahkan ini dengan rekursi:

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

Menggunakan contoh Anda:

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

Bagaimana dengan memeriksa dan mengatur elemen dict tanpa memproses semua indeks dua kali?

Larutan:

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

Contoh alur kerja:

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

Uji

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

Sangat terlambat ke pesta, tetapi memposting kalau-kalau ini dapat membantu seseorang di masa depan. Untuk kasus penggunaan saya, fungsi berikut bekerja paling baik. Berfungsi untuk menarik semua tipe data dari kamus

dict adalah kamus yang mengandung nilai kami

daftar adalah daftar "langkah-langkah" menuju nilai kami

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

Sangat memuaskan melihat jawaban ini karena memiliki dua metode statis untuk mengatur & mendapatkan atribut bersarang. Solusi ini jauh lebih baik daripada menggunakan pohon bersarang https://gist.github.com/hrldcpr/2012250

Inilah implementasi saya.

Penggunaan :

Untuk mengatur panggilan atribut bersarang sattr(my_dict, 1, 2, 3, 5) is equal to my_dict[1][2][3][4]=5

Untuk mendapatkan panggilan atribut bersarang 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
sumber
1

Saya sarankan Anda menggunakan python-benedictuntuk mengakses item bersarang menggunakan keypath.

Instal menggunakan pip:

pip install python-benedict

Kemudian:

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

Berikut dokumentasi lengkapnya: https://github.com/fabiocaccamo/python-benedict

Fabio Caccamo
sumber
0

Jika Anda juga ingin kemampuan untuk bekerja dengan json sewenang-wenang termasuk daftar dan dikte bersarang, dan menangani jalur pencarian yang tidak valid dengan baik, inilah solusi saya:

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

metode untuk merangkai string:

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

Memperluas pendekatan @DomTomCat dan lainnya, fungsional ini (yaitu, mengembalikan data yang dimodifikasi melalui deepcopy tanpa mempengaruhi input) setter dan mapper berfungsi untuk bersarang dictdan list.

penyetel:

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

Anda dapat menggunakan evalfungsi dalam python.

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

Penjelasan

Untuk kueri contoh Anda: maplist = ["b", "v", "y"]

nestqakan berada "nest['b']['v']['y']"di nesttempat kamus bersarang.

Fungsi evalbuiltin mengeksekusi string yang diberikan. Namun, penting untuk berhati-hati tentang kemungkinan kerentanan yang muncul dari penggunaan evalfungsi. Diskusi dapat ditemukan di sini:

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

Dalam nested_parse()fungsinya, saya telah memastikan bahwa tidak ada __builtins__global yang tersedia dan hanya variabel lokal yang tersedia adalah nestkamus.

Abhirup Das
sumber