12

Let's suppose that I have two dictionaries:

dic1 =  { "first":1, "second":4, "third":8} 
dic2 =  { "first":9, "second":5, "fourth":3}

Is there a straightforward way to obtain something like the below?

dic3 =  { "first":[1,9], "second":[4,5], "third":[8], "fourth":[3]}

I used lists to store values, but tuples are fine as well.

ggorlen
  • 44,755
  • 7
  • 76
  • 106
Netchaiev
  • 327
  • 1
  • 2
  • 14

8 Answers8

9

You can use a defaultdict to hold lists, and then just append the values to them. This approach easily extends to an arbitrary number of dictionaries.

from collections import defaultdict

dd = defaultdict(list)

dics = [dic1, dic2]
for dic in dics:
    for key, val in dic.iteritems():  # .items() in Python 3.
        dd[key].append(val)

>>> dict(dd)
{'first': [1, 9], 'fourth': [3], 'second': [4, 5], 'third': [8]}

All of the keys with a single value are still held within a list, which is probably the best way to go. You could, however, change anything of length one into the actual value, e.g.

for key, val in dd.iteritems():  # .items() in Python 3.
    if len(val) == 1
        dd[key] = val[0]
Alexander
  • 105,104
  • 32
  • 201
  • 196
  • 2
    This implementation will have all values in the resulting `defaultdict` be of type `list` even if there is no duplicate value within the key. If the preferred behavior is to leave single values as that values' type, then we should check for the existence of a key before adding and convert to a list only when that key already existed. – ctj232 Sep 28 '18 at 21:39
  • 1
    @ctj232 OP corrected question to have them in a list structure, which makes more sense anyway. – Alexander Sep 28 '18 at 21:55
9

Here's a naive solution; copy one of the dictionaries over to the result and iterate over the other dictionary's keys and values, adding lists to the result as necessary. Since there are only two dictionaries, no merged list will have more than 2 items.

dic1 = {"first": 1, "second": 4, "third": 8} 
dic2 = {"first": 9, "second": 5, "fourth": 3}
dic3 = dict(dic2)

for k, v in dic1.items():
    dic3[k] = [dic3[k], v] if k in dic3 else v

print(dic3) # => {'first': [9, 1], 'second': [5, 4], 'fourth': 3, 'third': 8}

If you'd like single values to be lists (likely better design; mixed types aren't much fun to deal with) you can use:

dic3 = {k: [v] for k, v in dic2.items()}

for k, v in dic1.items():
    dic3[k] = dic3[k] + [v] if k in dic3 else [v]

print(dic3) # => {'first': [9, 1], 'second': [5, 4], 'fourth': [3], 'third': [8]}

Generalizing it to any number of dictionaries:

def merge_dicts(*dicts):
    """
    >>> merge_dicts({"a": 2}, {"b": 4, "a": 3}, {"a": 1})
    {'a': [2, 3, 1], 'b': [4]}
    """
    merged = {}
    
    for d in dicts:
        for k, v in d.items():
            if k not in merged:
                merged[k] = []

            merged[k].append(v)
    
    return merged

You can use collections.defaultdict to clean it up a bit if you don't mind the import:

from collections import defaultdict

def merge_dicts(*dicts):
    """
    >>> merge_dicts({"a": 2}, {"b": 4, "a": 3}, {"a": 1})
    defaultdict(<class 'list'>, {'a': [2, 3, 1], 'b': [4]})
    """
    merged = defaultdict(list)
    
    for d in dicts:
        for k, v in d.items():
            merged[k].append(v)
    
    return merged
ggorlen
  • 44,755
  • 7
  • 76
  • 106
3

Given:

dic1 =  { "first":1, "second":4, "third":8} 
dic2 =  { "first":9, "second":5, "fourth":3}

You can use .setdefault:

dic_new={}
for k,v in list(dic1.items())+list(dic2.items()):
    dic_new.setdefault(k, []).append(v)
else:
    dic_new={k:v if len(v)>1 else v[0] for k,v in dic_new.items()}  

>>> dic_new
{'first': [1, 9], 'second': [4, 5], 'third': 8, 'fourth': 3}

This produces the output in question. I think that flattening the single elements lists to a different object type is an unnecessary complexity.


With the edit, this produces the desired result:

dic_new={}
for k,v in list(dic1.items())+list(dic2.items()):
    dic_new.setdefault(k, []).append(v)

>>> dic_new
{'first': [1, 9], 'second': [4, 5], 'third': [8], 'fourth': [3]}
dawg
  • 98,345
  • 23
  • 131
  • 206
3

Using set and dictionary comprehension

L = [d1, d2]
dups = set(d1.keys() & d2.keys())
d = {k: [L[0][k], L[1][k]] if k in dups else i[k] for i in L for k in i}
{'first': [1, 9], 'second': [4, 5], 'third': 8, 'fourth': 3}
jpp
  • 159,742
  • 34
  • 281
  • 339
vash_the_stampede
  • 4,590
  • 1
  • 8
  • 20
2

In general, I would say it's bad practice to cast the values of different keys as different object types. I would simply do something like:

def merge_values(val1, val2):
    if val1 is None:
        return [val2]
    elif val2 is None:
        return [val1]
    else:
        return [val1, val2]
dict3 = {
    key: merge_values(dic1.get(key), dic2.get(key))
    for key in set(dic1).union(dic2)
}
PMende
  • 5,171
  • 2
  • 19
  • 26
1

Create a new dictionary dic having for keys the keys of dic1 and dic2 and value an empty list, then iterate over dic1 and dic2 appending values to dic:

dic1 =  { "first":1, "second":4, "third":8} 
dic2 =  { "first":9, "second":5, "fourth":3}

dic = {key:[] for key in list(dic1.keys()) + list(dic2.keys())}

for key in dic1.keys():
    dic[key].append(dic1[key])

for key in dic2.keys():
    dic[key].append(dic2[key])
Scrooge McDuck
  • 372
  • 2
  • 14
  • Python 2 only as written. Python 3 would give a `TypeError` here with `dic1.keys() + dic2.keys()` – dawg Sep 28 '18 at 22:25
  • Converting dict.keys() to list gives compatibility with python3. I never searched why base functions like `sum` don't work anymore on arbitrary types in python3. `sum` over lists of strings was very convenient. – Scrooge McDuck Sep 28 '18 at 22:29
  • Now maybe make it a bit more efficient with `dic = {key:[] for key in {k for k in list(dic1) + list(dic2)}}` so that duplicate keys are eliminated. – dawg Sep 28 '18 at 22:34
  • Set conversion would still read the whole list to create the set. I didn't convert it because it would weigh down the notation even more and one would not gain anything on memory front. – Scrooge McDuck Sep 28 '18 at 22:48
  • Python is just trying to protect you. Using `sum` to concatenate a list of strings would be a classic example of a [Shlemiel the painter](https://www.joelonsoftware.com/2001/12/11/back-to-basics/) algorithm. Concatenating n strings would create n-2 temporary strings. – PM 2Ring Sep 28 '18 at 23:40
  • Protecting me from aliasing sum of strings to `''.join`? Sorry but I couldn't understand how the link relates to `sum`. The redundant calculations are described in the article as dependant on `+` implementation; so if it is the case for python, any concatenation of n strings that uses `+` is an efficient `join`. So why just not alias `+` between strings to a binary `''.join` `sum` of strings to normal `''.join` instead of keeping an inefficient string concatenator? – Scrooge McDuck Sep 30 '18 at 21:10
0

Solution for dict of lists (adapted from @dawg):

dic1 =  { "first":[1], "second":[4], "third":[8]} 
dic2 =  { "first":[9], "second":[5], "fourth":[3]}
dic_new={}
for k,v in list(dic1.items())+list(dic2.items()):
    dic_new.setdefault(k, []).extend(v)
>>> dic_new
{'first': [1, 9], 'second': [4, 5], 'third': [8], 'fourth': [3]}
BSalita
  • 8,420
  • 10
  • 51
  • 68
-1
from copy import deepcopy


def _add_value_to_list(value, lis):
    if value:
        if isinstance(value, list):
            lis.extend(value)
        else:
            lis.append(value)
    else:
        pass


def _merge_value(value_a, value_b):
    merged_value = []
    _add_value_to_list(value_a, merged_value)
    _add_value_to_list(value_b, merged_value)
    return merged_value


def _recursion_merge_dict(new_dic, dic_a, dic_b):
    if not dic_a or not dic_b:
        return new_dic
    else:
        if isinstance(new_dic, dict):
            for k, v in new_dic.items():
                new_dic[k] = _recursion_merge_dict(v, dic_a.get(k, {}), dic_b.get(k, {}))
            return new_dic
        else:
            return _merge_value(dic_a, dic_b)


def merge_dicts(dic_a, dic_b):
    new_dic = deepcopy(dic_a)
    new_dic.update(dic_b)

    return _recursion_merge_dict(new_dic, dic_a, dic_b)
Happy Boy
  • 526
  • 2
  • 9