Truy cập các mục từ điển lồng nhau thông qua một danh sách các khóa?


143

Tôi có một cấu trúc từ điển phức tạp mà tôi muốn truy cập thông qua một danh sách các khóa để giải quyết mục chính xác.

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

maplist = ["a", "r"]

hoặc là

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

Tôi đã tạo mã sau đây hoạt động nhưng tôi chắc chắn có một cách tốt hơn và hiệu quả hơn để làm điều này nếu bất cứ ai có ý tưởng.

# 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

Câu trả lời:


230

Sử dụng reduce()để duyệt từ điển:

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

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

và sử dụng lại getFromDictđể tìm vị trí lưu trữ giá trị cho setInDict():

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

Tất cả trừ phần tử cuối cùng mapListlà cần thiết để tìm từ điển 'cha mẹ' để thêm giá trị vào, sau đó sử dụng phần tử cuối cùng để đặt giá trị cho khóa bên phải.

Bản giới thiệu:

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

Lưu ý rằng hướng dẫn kiểu Python PEP8 quy định tên sn_case cho các hàm . Các cách trên hoạt động tốt như nhau cho các danh sách hoặc kết hợp từ điển và danh sách, vì vậy các tên nên thực sự get_by_path()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
Bao nhiêu đi qua như vậy là đáng tin cậy cho các cấu trúc lồng nhau tùy ý? Nó sẽ làm việc cho các từ điển hỗn hợp với danh sách lồng nhau? Làm cách nào để sửa đổi getFromDict () để cung cấp default_value và để default_value mặc định là Không có? Tôi là người mới làm quen với Python với nhiều năm phát triển PHP và trước khi phát triển C.
Dmitriy Sintsov

2
Ngoài ra, tập hợp ánh xạ lồng nhau sẽ tạo các nút không tồn tại, imo: liệt kê các khóa số nguyên, từ điển cho các khóa chuỗi.
Dmitriy Sintsov

1
@ user1353510: như đã xảy ra, cú pháp lập chỉ mục thông thường được sử dụng ở đây, vì vậy nó cũng sẽ hỗ trợ các danh sách bên trong từ điển. Chỉ cần vượt qua trong các chỉ số nguyên cho những người.
Martijn Pieters

1
@ user1353510: cho một giá trị mặc định, sử dụng try:, except (KeyError, IndexError): return default_valuexung quanh returndòng hiện tại .
Martijn Pieters

1
@Georgy: sử dụng dict.get()thay đổi ngữ nghĩa, vì nó trả về Nonethay vì tăng KeyErrorcho các tên bị thiếu. Bất kỳ tên tiếp theo sau đó kích hoạt một AttributeError. operatorlà một thư viện tiêu chuẩn, không cần phải tránh nó ở đây.
Martijn Pieters

40
  1. Giải pháp được chấp nhận sẽ không hoạt động trực tiếp cho python3 - nó sẽ cần một from functools import reduce.
  2. Ngoài ra có vẻ nhiều pythonic để sử dụng một forvòng lặp. Xem trích dẫn từ What New trong Python 3.0 .

    Đã xóa reduce(). Sử dụng functools.reduce()nếu bạn thực sự cần nó; tuy nhiên, 99 phần trăm thời gian một forvòng lặp rõ ràng là dễ đọc hơn.

  3. Tiếp theo, giải pháp được chấp nhận không đặt các khóa lồng nhau không tồn tại (nó trả về a KeyError) - xem câu trả lời của @ eafit để biết giải pháp

Vậy tại sao không sử dụng phương pháp được đề xuất từ ​​câu hỏi của kolergy để nhận giá trị:

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

Và mã từ câu trả lời của @ eafit để đặt giá trị:

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

Cả hai hoạt động thẳng trong python 2 và 3


6
Tôi thích giải pháp này - nhưng hãy cẩn thận. Nếu tôi không nhầm, vì từ điển Python không phải là bất biến getFromDictcó khả năng phá hủy trình gọi dataDict. Tôi sẽ copy.deepcopy(dataDict)đầu tiên. Tất nhiên, (như đã viết) hành vi này được mong muốn trong chức năng thứ hai.
Dylan F

15

Sử dụng giảm là thông minh, nhưng phương thức thiết lập của OP có thể có vấn đề nếu các khóa cha không tồn tại trước trong từ điển lồng nhau. Vì đây là bài viết SO đầu tiên tôi thấy cho chủ đề này trong tìm kiếm google của mình, tôi muốn làm cho nó tốt hơn một chút.

Phương thức thiết lập trong ( Đặt giá trị trong từ điển python lồng nhau được cung cấp danh sách các chỉ số và giá trị ) có vẻ mạnh mẽ hơn khi thiếu các khóa cha mẹ. Để sao chép nó qua:

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

Ngoài ra, có thể thuận tiện khi có một phương thức đi ngang qua cây khóa và có được tất cả các đường dẫn khóa tuyệt đối mà tôi đã tạo:

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

Một cách sử dụng của nó là để chuyển đổi cây lồng nhau thành DataFrame của gấu trúc, sử dụng mã sau đây (giả sử rằng tất cả các lá trong từ điển lồng nhau có cùng độ sâu).

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)

tại sao lại tự ý giới hạn độ dài đối số 'khóa' xuống còn 2 hoặc nhiều hơn nested_set?
alancalvitti

10

Thư viện này có thể hữu ích: https://github.com/akesterson/dpath-python

Một thư viện python để truy cập và tìm kiếm từ điển thông qua / slashing / path ala xpath

Về cơ bản, nó cho phép bạn toàn cầu trong một từ điển như thể nó là một hệ thống tập tin.


3

Làm thế nào về việc sử dụng các hàm đệ quy?

Để có được một giá trị:

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]

Và để đặt giá trị:

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

Kiểu Python thuần túy, không cần nhập bất kỳ:

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)

Đầu ra

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

2

Một cách khác nếu bạn không muốn đưa ra lỗi nếu thiếu một trong các khóa (để mã chính của bạn có thể chạy mà không bị gián đoạ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

Trong trường hợp này, nếu không có bất kỳ khóa đầu vào nào, Không có trả lại, có thể được sử dụng làm kiểm tra trong mã chính của bạn để thực hiện một tác vụ thay thế.


1

Thay vì thực hiện một lần nhấn hiệu suất mỗi lần bạn muốn tìm kiếm một giá trị, làm thế nào về việc bạn làm phẳng từ điển một lần sau đó chỉ cần tra cứu khóa như 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}

Bằng cách này, bạn có thể chỉ cần tra cứu các mục bằng cách sử dụng flat_dict['b:v:y']sẽ cung cấp cho bạn 1.

Và thay vì duyệt từ điển trên mỗi lần tra cứu, bạn có thể tăng tốc độ này bằng cách làm phẳng từ điển và lưu kết quả đầu ra để tra cứu từ khởi động nguội có nghĩa là tải từ điển đã làm phẳng và chỉ cần thực hiện tra cứu khóa / giá trị mà không cần truyền tải.


1

Giải quyết điều này với đệ quy:

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

Sử dụng ví dụ của bạn:

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

Làm thế nào về kiểm tra và sau đó thiết lập phần tử dict mà không xử lý tất cả các chỉ mục hai lần?

Giải pháp:

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

Ví dụ quy trình làm việc:

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

Kiểm tra

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

Rất muộn cho bữa tiệc, nhưng đăng trong trường hợp này có thể giúp đỡ ai đó trong tương lai. Đối với trường hợp sử dụng của tôi, chức năng sau đây hoạt động tốt nhất. Hoạt động để kéo bất kỳ loại dữ liệu ra khỏi từ điển

dict là từ điển chứa giá trị của chúng tôi

danh sách là danh sách "các bước" hướng tới giá trị của chúng tôi

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

Thật hài lòng khi thấy những câu trả lời này vì có hai phương thức tĩnh để thiết lập và nhận các thuộc tính lồng nhau. Những giải pháp này tốt hơn nhiều so với sử dụng cây lồng nhau https://gist.github.com/hrldcpr/2012250

Đây là triển khai của tôi.

Cách sử dụng :

Để đặt cuộc gọi thuộc tính lồng nhau sattr(my_dict, 1, 2, 3, 5) is equal to my_dict[1][2][3][4]=5

Để có được một cuộc gọi thuộc tính lồng nhau 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

Tôi đề nghị bạn sử dụng python-benedictđể truy cập các mục lồng nhau bằng cách sử dụng phím.

Cài đặt nó bằng cách sử dụng pip:

pip install python-benedict

Sau đó:

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

Dưới đây là tài liệu đầy đủ: https://github.com/fabiocaccamo/python-benedict


0

Nếu bạn cũng muốn khả năng làm việc với json tùy ý bao gồm các danh sách và các ký tự lồng nhau và xử lý độc đáo các đường dẫn tra cứu không hợp lệ, đây là giải pháp của tôi:

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

một phương pháp để nối chuỗi:

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

Mở rộng @DomTomCat và cách tiếp cận của người khác, các chức năng này (nghĩa là trả lại dữ liệu đã sửa đổi thông qua deepcopy mà không ảnh hưởng đến đầu vào) setter và mapper hoạt động cho lồng nhau dictlist.

thiết lập:

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

người vẽ bản đồ:

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

Bạn có thể sử dụng evalchức năng trong python.

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

Giải trình

Đối với truy vấn ví dụ của bạn: maplist = ["b", "v", "y"]

nestqsẽ là "nest['b']['v']['y']"nơi nesttừ điển lồng nhau.

Hàm evaldựng sẵn thực thi chuỗi đã cho. Tuy nhiên, điều quan trọng là phải cẩn thận về các lỗ hổng có thể phát sinh từ việc sử dụng evalchức năng. Thảo luận có thể được tìm thấy ở đây:

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

Trong nested_parse()hàm, tôi đã đảm bảo rằng không __builtins__có toàn cục nào khả dụng và chỉ có biến cục bộ có sẵn là nesttừ điển.


Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.