将dict合并到另一个,覆盖值,包括更新列表和子字节(不覆盖列表本身)

时间:2017-05-31 21:06:29

标签: python python-2.7 list dictionary

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

我还有一个小的首选词典P,它包含这个词典的任意子集(我100%确定这是一个完美的子集)。

我希望将此子集P合并到默认字典D上。

我认为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。层次结构非常深入,我在列表内的dicts内部列出了内容。此外,原子更新规则显然不清楚(即0到10是添加还是覆盖?)。要清楚原子更新是否会被覆盖。 P覆盖D.它只是需要进一步迭代的dicts和dicts列表。 (我希望用户首选项覆盖默认设置有助于可视化)。我在上面的玩具示例中也省略了一个重要的细节,那就是列表中的词典不应该与键名相匹配(如上例所示,即带键的词典' a'是P和D共有,但是按特定键的值。请参阅下面的新玩具示例。

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解决方案,但现在这样做。

2 个答案:

答案 0 :(得分:0)

这是一个合并你当前的两个dicts的黑客攻击。

我知道这不是“最pythonic”的方式,但它可以处理像你这样的dicts并提供所需的输出。

在我的回答中,我正在使用来自itertools groupby的{​​{1}}和zip_longest

以下是我的回答:

module

输出:

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)

答案 1 :(得分:0)

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

注意:下面的D和P是非常简化的玩具示例。实际上,他们在列表内部的列表内的列表中非常深入。这可能不会涵盖所有个案,但它似乎适用于我的数据(保存到json时约为50KB)。

输出:

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