pythondictionarymapping

Merge dictionaries with same key using ChainMap


I have two dictionaries

a = {'123': {'player': 1,
             'opponent': 2},
     '18': {'player': 10,
            'opponent': 12}
    }


b = {'123': {'winner': 1},
     '180': {'winner': 2}
    }

The goal is to merge them together and get one dictionary that looks as follows:

{'123': {'player': 1,
         'opponent': 2,
         'winner': 1},
 '18': {'player': 10,
        'opponent': 12},
 '180': {'winner': 2}
}

I would like to use collections.ChainMap for this purpose. I tried the following

>>> from collections import ChainMap
>>> print(dict(ChainMap(a, b)))
{'123': {'player': 1, 'opponent': 2}, '180': {'winner': 2}, '18': {'player': 10, 'opponent': 12}}

In the docs they create a new class as follows:

class DeepChainMap(ChainMap):
    'Variant of ChainMap that allows direct updates to inner scopes'

    def __setitem__(self, key, value):
        for mapping in self.maps:
            if key in mapping:
                mapping[key] = value
                return
        self.maps[0][key] = value

    def __delitem__(self, key):
        for mapping in self.maps:
            if key in mapping:
                del mapping[key]
                return
        raise KeyError(key)

>>> print(dict(DeepChainMap(a, b)))
{'123': {'player': 1, 'opponent': 2}, '180': {'winner': 2}, '18': {'player': 10, 'opponent': 12}}

How can I modify the class and get the desired output?


Solution

  • You can create your own DeepChainMap with a __getitem__ method that recursively constructs a DeepChainMap object from all mappings with the given key if the value of the first mapping that has the given key is a dict:

    from collections import ChainMap
    
    class DeepChainMap(ChainMap):
        def __getitem__(self, key):
            values = (mapping[key] for mapping in self.maps if key in mapping)
            try:
                first = next(values)
            except StopIteration:
                return self.__missing__(key)
            if isinstance(first, dict):
                return self.__class__(first, *values)
            return first
    
        def __repr__(self): # convert to dict for a cleaner representation
            return repr(dict(self))
    

    so that:

    print(DeepChainMap(a, b))
    

    outputs:

    {'123': {'winner': 1, 'player': 1, 'opponent': 2}, '180': {'winner': 2}, '18': {'player': 10, 'opponent': 12}}
    

    Demo: https://ideone.com/pdicfc