将一个命令合并为另一个覆盖值,包括更新列表和偏差(而不是覆盖列表本身)



我有一个字典D,其中包含我应用程序的默认设置。它具有复杂的层次结构,例如列表,以及这些列表中的更多内容(例如,它可能具有模块列表,并且在每个模块中都有进一步的dict,有时还有更多列表和更多dicts等(。

我也有一个小的偏好字典P,其中包含该dict的任意子集(我100%确定这是一个完美的子集(。

我想在默认字典D上合并此子集P。

我以为D.Update(P(会起作用,但这覆盖了列表。例如。

D={'i':0, 'j':1, 'modules':[{'a':1}, {'b':2}, {'c':3}] }
P={'i':10, 'modules':[{'c':30}] }
D.update()
# gives {'i': 10, 'j': 1, 'modules': [{'c': 30}]}
# I'd like {'i': 10, 'j': 1, 'modules': [{'a': 1}, {'b': 2}, {'c': 30}]}

关于以不同方式合并词典,添加条目等方面有很多类似的帖子,但它们似乎都没有解决这个确切的问题。这似乎是一项非常普遍的任务,但我不知道该怎么做,所以我感谢任何指示。欢呼,

(P.S。我也想维护所有列表的顺序,因为它在GUI中得到了反映(

编辑:看来我的解释并不清楚。对于那个很抱歉。上面的示例是一个非常简单的玩具示例。我的实际数据(保存到JSON时(约为50K。层次结构非常深入,我在列表中内部的列表中内部列表等。原子更新规则显然不清楚(即0到10是添加或覆盖吗?(。为了明确,原子更新正在覆盖。p覆盖D。只是命令和列表,需要进一步迭代。(我希望用户偏好覆盖默认设置将有助于可视化(。我还省略了上面玩具示例中的一个重要细节,也就是说,列表中的词典不应用钥匙名匹配(如上面的示例,即用键'a''(,但按照特定键的价值。请参阅下面的新玩具示例。

D={'i':'Hello', 'j':'World', 'modules':[{'name':'a', 'val':1}, {'name':'b', 'val':2}, {'name':'c', 'val':3}, {'name':'d', 'val':4}] }
P={'i':'Goodbye', 'modules':[{'name':'a', 'val':10}, {'name':'c', 'val':30}] }

edit2 :我添加了一个似乎有效的解决方案。我希望有一个更简洁的Pythonic解决方案,但这暂时可以完成工作。

这是合并您当前两个dicts的黑客。

我知道这不是"大多数Pythonic"的方法,但是它可以像您一样处理dicts并给出所需的输出。

在我的回答中,我正在使用Itertools modulegroupbyzip_longest

这是我的答案:

from itertools import groupby, zip_longest
D = {'i':0, 'j':1, 'modules':[{'a':1}, {'b':2}, {'c':3}] }
P = {'i':10, 'modules':[{'c':30}] }
sub = list(D.items()) + list(P.items())
final = {}
for k,v in groupby(sorted(sub, key=lambda x: x[0]), lambda x: x[0]):
    bb = list(v)
    if not isinstance(bb[0][1], list):
        for j in bb:
            final[k] = max(bb, key=lambda x: x[1])[1]
    else:
        kk, ff = [], []
        for k_ in zip_longest(*[k[1] for k in bb]):
            kk += [j for j in k_ if j != None] 
        for j,m in groupby(sorted(kk, key= lambda x: list(x.keys())[0]), lambda x: list(x.keys())[0]):
            ff += ff += [dict(max([list(k.items()) for k in list(m)], key=lambda x:x))]
        final[k] = ff
print(final)

输出:

{'i': 10, 'j': 1, 'modules': [{'a': 1}, {'b': 2}, {'c': 30}]}

我希望有更多的Pythonic解决方案(更简洁(。这是一个类似C的解决方案(这是我来自的地方(。

注意:下面的D和P是非常简化的玩具示例。实际上,它们非常深入,内部列表内部列表内部列表内部列表。这可能并不涵盖所有案例,但它似乎可以与我的数据一起使用(保存到JSON时〜50kbish(。

输出:

In [2]: P
Out[2]: 
{'i': 'Goodbye',
 'modules': [{'name': 'a', 'val': 10}, {'name': 'c', 'val': 30}]}
In [3]: D
Out[3]: 
{'i': 'Hello',
 'j': 'World',
 'modules': [{'name': 'a', 'val': 1},
  {'name': 'b', 'val': 2},
  {'name': 'c', 'val': 3},
  {'name': 'd', 'val': 4}]}
In [4]: merge_dicts_by_name(P, D)
 merge_dicts_by_name  <type 'dict'> <type 'dict'>
 key: .i : Hello overwritten by Goodbye
 key: .modules : 
    merge_dicts_by_name .modules <type 'list'> <type 'list'>
    list item: .modules[0]
       merge_dicts_by_name .modules[0] <type 'dict'> <type 'dict'>
       key: .modules[0].name : a overwritten by a
       key: .modules[0].val : 1 overwritten by 10
    list item: .modules[1]
       merge_dicts_by_name .modules[1] <type 'dict'> <type 'dict'>
       key: .modules[1].name : c overwritten by c
       key: .modules[1].val : 3 overwritten by 30
In [5]: D
Out[5]: 
{'i': 'Goodbye',
 'j': 'World',
 'modules': [{'name': 'a', 'val': 10},
  {'name': 'b', 'val': 2},
  {'name': 'c', 'val': 30},
  {'name': 'd', 'val': 4}]}

代码:

def merge_dicts_by_name(P, D, id_key='name', root='', depth=0, verbose=True, indent='   '):
    '''
    merge from dict (or list of dicts) P into D. 
    i.e. can think of D as Default settings, and P as a subset containing user Preferences.
    Any value in P or D can be a dict or a list of dicts
    in which case same behaviour will apply (through recursion):
        lists are iterated and dicts are matched between P and D
        dicts are matched via an id_key (only at same hierarchy depth / level)
        matching dicts are updated with same behaviour
        for anything else P overwrites D
    P : dict or list of dicts (e.g. containing user Preferences, subset of D)
    D : dict or list of dicts (e.g. Default settings)
    id_key : the key by which sub-dicts are compared against (e.g. 'name')
    root : for keeping track of full path during recursion
    depth : keep track of recursion depth (for indenting)
    verbose : dump progress to console
    indent : with what to indent (if verbose)
    '''
    if verbose:
        indent_full = indent * depth
        print(indent_full, 'merge_dicts_by_name', root, type(P), type(D))
    if type(P)==list: # D and P are lists of dicts
        assert(type(D)==type(P))
        for p_i, p_dict in enumerate(P): # iterate dicts in P
            path = root + '[' + str(p_i) + ']'
            if verbose: print(indent_full, 'list item:', path)
            d_id = p_dict[id_key] # get name of current dict
            # find corresponding dict in D 
            d_dict = D[ next(i for (i,d) in enumerate(D) if d[id_key] == d_id) ]
            merge_dicts_by_name(p_dict, d_dict, id_key=id_key, root=path, depth=depth+1, verbose=verbose, indent=indent)
    elif type(P)==dict:
        assert(type(D)==type(P))
        for k in P:
            path = root + '.' + k
            if verbose: print(indent_full, 'key:', path, end=' : ')
            if k in D:
                if type(P[k]) in [dict, list]:
                    print()
                    merge_dicts_by_name(P[k], D[k], id_key=id_key, root=path, depth=depth+1, verbose=verbose, indent=indent)
                else:
                    if verbose: print(D[k], 'overwritten by', P[k])
                    D[k] = P[k]
            else:
                print(indent_full, 'Warning: Key {} in P not found in D'.format(path))
    else:
        print(indent_full, "Warning: Don't know what to do with these types", type(P), type(D))

最新更新