SimonL
SimonL

Reputation: 1838

Merging hierarchy of dictionaries in Python

I have two dictionaries, and what I'm trying to do is a bit odd. Basically, I want to merge them. That's simple enough. But they're hierarchies of of dictionaries, and I want to merge them in such a way that if an item in a dictionary is itself a dictionary and exists in both, I want to merge those dictionaries as well. If it's not a dictionary, I want the values from the second dictionary to overwrite the values from the first one. Something sort of like this:

a = {0: {0: "a"},
     1: [0, 1, 2]}

b = {0: {1: "b"},
     1: [3, 4, 5]}

Merge(a, b)

#output:
{0: {0: "a",
     1: "b"},
 1: [3, 4, 5]}

Does that make sense? Because the key "0" contained a dictionary in both a and b, it merged those dictionaries as well. But in the case of the second key, it was a list so it just overwrote it.

So I suppose I'll be looking at some kind of recursive function? Not quite sure how to approach this one.

Thanks!

Edit: I forgot to mention one pretty crucial detail:

I need a function that works in both 2.6.2 and 2.7.3.

Upvotes: 2

Views: 1720

Answers (3)

Hans Bouwmeester
Hans Bouwmeester

Reputation: 1519

Needed something similar and implemented a more straightforward recursive solution. In-place updates dict 'd'.

from collections.abc import MutableMapping

def merge(d, v):
    """
    Merge two dictionaries.
    
    Merge dict-like `v` into dict-like `d`. In case keys between them are the same, merge
    their sub-dictionaries where possible. Otherwise, values in `v` overwrite `d`.
    """
    for key in v:
        if key in d and isinstance(d[key], MutableMapping) and isinstance(v[key], MutableMapping):
            d[key] = merge(d[key], v[key])
        else:
            d[key] = v[key]
    return d

Example 1:

a = {0: {0: "a"},
     1: [0, 1, 2]}

b = {0: {1: "b"},
     1: [3, 4, 5]}
     
>>> merge(a, b)
{0: {0: 'a', 1: 'b'}, 1: [3, 4, 5]}

Example 2:

a = {0: {0: 'a'},
     1: [0, 1, 2],
     2: [9, 9],
     3: {'a': {1: 1, 2: 2}, 'b': [0, 1]}}

b = {0: {1: 'b'},
     1: [3, 4, 5],
     2: {22: 22, 33: 33},
     3: {'a': {2: 22, 3: 33}, 'b': [99, 88]}}
     
>>> merge(a, b) 
{0: {0: 'a', 1: 'b'},
 1: [3, 4, 5],
 2: {22: 22, 33: 33},
 3: {'a': {1: 1, 2: 22, 3: 33}, 'b': [99, 88]}}

Upvotes: 0

Jared
Jared

Reputation: 26397

Assuming you might have nested dictionaries (based on your thinking in terms of recursion), something like this should work,

from copy import deepcopy

def merge(a, b):
    if isinstance(b, dict) and isinstance(a, dict):
        a_and_b = a.viewkeys() & b.viewkeys()
        every_key = a.viewkeys() | b.viewkeys()
        return {k: merge(a[k], b[k]) if k in a_and_b else 
                   deepcopy(a[k] if k in a else b[k]) for k in every_key}
    return deepcopy(b)

The return value of merge(a, b) is conceptually like creating a (deep) copy of a and running a recursive version of a.update(b).


Using some nested examples,

a = {0: {0: 'a'},
     1: [0, 1, 2],
     2: [9, 9],
     3: {'a': {1: 1, 2: 2}, 'b': [0, 1]}}

b = {0: {1: 'b'},
     1: [3, 4, 5],
     2: {22: 22, 33: 33},
     3: {'a': {2: 22, 3: 33}, 'b': [99, 88]}}

merge(a, b) produces,

{0: {0: 'a', 1: 'b'},
 1: [3, 4, 5],
 2: {22: 22, 33: 33},
 3: {'a': {1: 1, 2: 22, 3: 33}, 'b': [99, 88]}}

EDIT: Python 2.6 version

def merge(a, b):
    if isinstance(b, dict) and isinstance(a, dict):
        a_and_b = set(a).intersection(b)
        every_key = set(a).union(b)
        return dict((k, merge(a[k], b[k]) if k in a_and_b else
                     deepcopy(a[k] if k in a else b[k])) for k in every_key)
    return deepcopy(b)

Upvotes: 1

roippi
roippi

Reputation: 25954

Hmm.. as long as you're not arbitrarily-nested, you don't need recursion.

from itertools import chain

{k:(v if not isinstance(v,dict) else dict(chain(a[k].items(), v.items()))) for k,v in b.items()}
Out[10]: {0: {0: 'a', 1: 'b'}, 1: [3, 4, 5]}

(I'm using python 3 here, feel free to replace .items with .iteritems in python 2)

Since this is a bit verbose, there's always the sneaky way to merge two dicts:

{k:(v if not isinstance(v,dict) else dict(a[k], **v)) for k,v in b.items()}
Out[11]: {0: {0: 'a', 1: 'b'}, 1: [3, 4, 5]}

You may or may not want to use this syntax - though it is compact, it kind of abuses cPython implementation details.

Upvotes: 0

Related Questions