简体   繁体   English

我可以使用键列表访问嵌套的dict吗?

[英]Can I access a nested dict with a list of keys?

I would like to access a dictionary programmatically. 我想以编程方式访问字典。 I know how to do this with a recursive function, but is there a simpler way? 我知道如何使用递归函数执行此操作,但有更简单的方法吗?

example = {'a': {'b': 'c'},
           '1': {'2': {'3': {'4': '5'}}}}

keys = ('a', 'b')
example[keys] = 'new'
# Now it should be
#     example = {'a': {'b': 'new'},
#                '1': {'2': {'3': {'4': '5'}}}}


keys = ('1', '2', '3', '4')
example[keys] = 'foo'
# Now it should be
#     example = {'a': {'b': 'new'},
#                '1': {'2': {'3': {'4': 'foo'}}}}


keys = ('1', '2')
example[keys] = 'bar'
# Now it should be
#     example = {'a': {'b': 'new'},
#                '1': {'2': 'bar'}}

What you seem to want to do is define your own class of dictionary that supports this kind of indexing. 你似乎想要做的是定义你自己的支持这种索引的字典类。 We can attain a fairly neat syntax by using the fact that when you do d[1, 2, 3] , Python actually passes the tuple (1, 2, 3) to __getitem__ . 我们可以通过使用这样的事实来获得相当简洁的语法:当你执行d[1, 2, 3] ,Python实际上将元组(1, 2, 3)传递给__getitem__

class NestedDict:
    def __init__(self, *args, **kwargs):
        self.dict = dict(*args, **kwargs)

    def __getitem__(self, keys):
        # Allows getting top-level branch when a single key was provided
        if not isinstance(keys, tuple):
            keys = (keys,)

        branch = self.dict
        for key in keys:
            branch = branch[key]

        # If we return a branch, and not a leaf value, we wrap it into a NestedDict
        return NestedDict(branch) if isinstance(branch, dict) else branch

    def __setitem__(self, keys, value):
        # Allows setting top-level item when a single key was provided
        if not isinstance(keys, tuple):
            keys = (keys,)

        branch = self.dict
        for key in keys[:-1]:
            if not key in branch:
                branch[key] = {}
            branch = branch[key]
        branch[keys[-1]] = value

Here are examples of usage 以下是使用示例

# Getting an item
my_dict = NestedDict({'a': {'b': 1}})
my_dict['a', 'b'] # 1

# Setting an item
my_dict = NestedDict()
my_dict[1, 2, 3] = 4
my_dict.dict # {1: {2: {3: 4}}}

# You can even get a branch
my_dict[1] # NestedDict({2: {3: 4}})
my_dict[1][2, 3] # 4

You can then make NestedDict implementation richer by also defining __iter__ , __len__ and __contains__ . 然后,您可以NestedDict实现由定义也更加丰富__iter____len____contains__

Also, this can be integrated fairly easily in your code since any pre-existing dictionary can be turned into a nested one by doing NestedDict(your_dict) . 此外,这可以很容易地集成到您的代码中,因为任何预先存在的字典都可以通过执行NestedDict(your_dict)转换为嵌套字典。

This solution creates another dictionary with same keys and then updates the existing dictionary: 此解决方案创建具有相同键的另一个字典,然后更新现有字典:

#!/usr/bin/env python

from six.moves import reduce


def update2(input_dictionary, new_value, loc):
    """
    Update a dictionary by defining the keys.

    Parameters
    ----------
    input_dictionary : dict
    new_value : object
    loc : iterable
        Location

    Returns
    -------
    new_dict : dict

    Examples
    --------
    >>> example = {'a': {'b': 'c'}, '1': {'2': {'3': {'4': '5'}}}}

    >>> update2(example, 'new', ('a', 'b'))
    {'a': {'b': 'new'}, '1': {'2': {'3': {'4': '5'}}}}

    >>> update2(example, 'foo', ('1', '2', '3', '4'))
    {'a': {'b': 'new'}, '1': {'2': {'3': {'4': 'foo'}}}}

    >>> update2(example, 'bar', ('1', '2'))
    {'a': {'b': 'new'}, '1': {'2': 'bar'}}
    """
    new_dict = reduce(lambda x, y: {y: x}, reversed(loc), new_value)
    input_dictionary.update(new_dict)
    return input_dictionary

if __name__ == '__main__':
    import doctest
    doctest.testmod()

use string, list or tuple for access keys 使用字符串,列表或元组来访问密钥

After adapting this answer (for cosmetic essentially) I got: 在适应了这个答案 (基本上是化妆品)后,我得到了:

from functools import reduce
import operator

def nested_get(dictionary, *keys):
    return reduce(operator.getitem, keys, dictionary)

def nested_set(dictionary, value, *keys):
    nested_get(dictionary, *keys[:-1])[keys[-1]] = value


example = {
    'a': {'b': 'c'},
    '1': {
        '2': {
            '3': {'4': '5'}
        }
    }
}

nested_set(example, "foo", "1", "2", "3")
print(example)

keys = ["1", "2"]
nested_set(example, "yay", *keys)
print(example)

Output: 输出:

{'a': {'b': 'c'}, '1': {'2': {'3': 'foo'}}}
{'a': {'b': 'c'}, '1': {'2': 'yay'}}

Same idea (to use the fact that the variables are passed by reference), but this time adapting one of my answers : 相同的想法(使用变量通过引用传递的事实),但这次调整我的一个答案

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" } } }
nested_set(example, "yay", "foo", "bar")
print(example)

Output 产量

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

This one does not requires any fancy imports, so I tend to like it more.. Chose your flavor 这个不需要任何花哨的进口,所以我倾向于更喜欢它..选择你的味道

You can use a smaller recursive function with a dictionary comprehension: 您可以使用较小的递归函数和字典理解:

import functools
def all_examples(f):
   def wrapper():
      def update_dict(d, path, target):
         return {a:target if path[-1] == a else update_dict(b, path, target) if isinstance(b, dict) else b for a, b in d.items()}
      current_d = {'a': {'b': 'c'},'1': {'2': {'3': {'4': '5'}}}}
      final_ds = []          
      for i in f():
         current_d = update_dict(current_d, *i)
         final_ds.append(current_d)
      return final_ds
   return wrapper

@all_examples
def input_data():
  return [[('a', 'b'), 'new'], [('1', '2', '3', '4'), 'foo'], [('1', '2'), 'bar']]

for i in input_data():
  print(i)

Output: 输出:

{'a': {'b': 'new'}, '1': {'2': {'3': {'4': '5'}}}}
{'a': {'b': 'new'}, '1': {'2': {'3': {'4': 'foo'}}}}
{'a': {'b': 'new'}, '1': {'2': 'bar'}}

Make traversal a method of your dictionary. 遍历遍历词典的方法。 You can do this easily by subclassing dict . 您可以通过继承dict轻松完成此操作。

The algorithm for traversing is courtesy of @MartijnPeters (upvote there). 遍历算法由@MartijnPeters提供 (在那里upvote)。

import operator

class ndict(dict):

    def get_traverse(self, mapList):
        return reduce(operator.getitem, mapList, self)

    def set_traverse(self, mapList, value):
        self.get_traverse(mapList[:-1])[mapList[-1]] = value

d = ndict({'a': {'b': 'c'}, '1': {'2': {'3': {'4': '5'}}}})

d.get_traverse(['a', 'b'])     # 'c'
d.set_traverse(['a', 'b'], 4)  # {'a': {'b': 4}, '1': {'2': {'3': {'4': '5'}}}}

I wanted to add a reference to dict-digger which is an open-source module I sometimes like to use that provides this functionality out of the box. 我想添加对dict-digger的引用,这是一个我有时喜欢使用的开源模块,可以提供开箱即用的功能。 (I am in no way associated to that project) (我与该项目无关)

I combined jpp and Olivier Melançon answer to create this a NestedDict class. 我结合了jpp和OlivierMelançon的回答来创建一个NestedDict类。 I prefer to access a dict like you normally access a dict but then with a list as argument 我更喜欢访问像你通常访问dict那样的dict,然后以列表作为参数

import operator
from functools import reduce

class NestedDict(dict):
    def __getitem__(self, item):
        if isinstance(item, list):
            return self.get_traverse(item)
        return super(NestedDict, self).__getitem__(item)

    def __setitem__(self, key, value):
        if isinstance(key, list):
            for _key in key[:-1]:
                if not _key in self:
                    self[_key] = {}
                self = self[_key]
            self[key[-1]] = value
            return self
        return super(NestedDict, self).__setitem__(key, value)

    def get_traverse(self, _list):
        return reduce(operator.getitem, _list, self)



nested_dict = NestedDict({'aggs': {'aggs': {'field_I_want': 'value_I_want'}, 'None': None}})
path = ['aggs', 'aggs', 'field_I_want']
nested_dict[path]  # 'value_I_want'

nested_dict[path] = 'changed'
nested_dict[path]  # 'changed'

Edit 编辑

For anyone that is interested. 对于任何有兴趣的人。 I enhanced the class with a function to automatically find paths. 我通过自动查找路径的功能增强了类。 (Usage in the doctest) (doctest中的用法)

import operator
from functools import reduce
from copy import deepcopy

class NestedDict(dict):
    """
    Dictionary that can use a list to get a value

    :example:
    >>> nested_dict = NestedDict({'aggs': {'aggs': {'field_I_want': 'value_I_want'}, 'None': None}})
    >>> path = ['aggs', 'aggs', 'field_I_want']
    >>> nested_dict[path]
    'value_I_want'
    >>> nested_dict[path] = 'changed'
    >>> nested_dict[path]
    'changed'
    """
    def __getitem__(self, item):
        if isinstance(item, list):
            return self.get_traverse(item)
        return super(NestedDict, self).__getitem__(item)

    def __setitem__(self, key, value):
        if isinstance(key, list):
            for _key in key[:-1]:
                if _key not in self:
                    self[_key] = {}
                self = self[_key]
            self[key[-1]] = value
            return self
        return super(NestedDict, self).__setitem__(key, value)

    def get_traverse(self, _list):
        return reduce(operator.getitem, _list, self)

    _paths = []
    _path = []
    def find(self, key, _dict=None, _main_loop=True):
        """ Find a list of paths to a key

        :param key: str, the key you want to find
        :param _dict: used for recursive searching
        :return: list with paths
        :example:
        >>> nested_dict = NestedDict({'aggs': {'aggs': {'field_I_want': 'value_I_want'}, 'None': None}})
        >>> paths = nested_dict.find('field_I_want')
        >>> paths
        [['aggs', 'aggs', 'field_I_want']]
        >>> nested_dict[paths[0]] = 'changed'
        >>> nested_dict[paths[0]]
        'changed'
        """
        if _main_loop:
            self._path, self._paths = [], []
            _dict = self

        for _key in _dict.keys():        
            self._path.append(_key)

            if _key == key:
                self._paths.append(deepcopy(self._path)) 

            if isinstance(_dict[_key], dict):
                self.find(key, _dict[_key], _main_loop=False)

            self._path.pop()

        if _main_loop:
            return self._paths

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM