9

Assuming that I have 3 different dictionaries:

dict1 = {
  "A": "a"
}

dict2 = {
  "B": "b", 
  "C": "c",
  "D": "d", 
  "E": "e"
}

dict3 = {
  "F": "f", 
  "G": "g"
}

I want to compute the product of these dictionaries (excluding the product between dict2 and dict3) and combine both the keys and values where the keys are concatenated with _ and values with ' and '

The desired output would be a single dictionary:

{
  # dict1 x dict2 
  "A_B": "a and b", 
  "A_C": "a and c",
  "A_D": "a and d",
  "A_E": "a and e",

  # dict1 x dict3  
  "A_F": "a and f",
  "A_G": "a and g",

  # dict1 x dict2 x dict3 
  "A_B_F": "a and b and f",
  "A_B_G": "a and b and g",
  "A_C_F": "a and c and f",
  "A_C_G": "a and c and g",
  "A_D_F": "a and d and f",
  "A_D_G": "a and d and g",
  "A_E_F": "a and e and f",
  "A_E_G": "a and e and g"
} 

I had a look at the documentation for itertools but I was not able to understand how I can achieve the desired output.

Tokyo
  • 753
  • 1
  • 10
  • 25

4 Answers4

8

The function that will do the job is itertools.product. First, here is how you can print out the product dict1 x dict2 x dict3:

for t in product(dict1.items(), dict2.items(), dict3.items()): 
     k, v = zip(*t) 
     print("_".join(k), "-", " and ".join(v))   

Output:

A_B_F - a and b and f
A_B_G - a and b and g
A_C_F - a and c and f
A_C_G - a and c and g
A_D_F - a and d and f
A_D_G - a and d and g
A_E_F - a and e and f
A_E_G - a and e and g

Now, just populate a result dictionary:

result = {}
for t in product(dict1.items(), dict2.items(), dict3.items()): 
     k, v = zip(*t) 
     result["_".join(k)] = " and ".join(v)

You can now add to this dictionary the dict1 x dict2 and dict1 x dict3 products, that are even simpler to compute.


Based on @ShadowRanger's comment, here is a complete snippet:

import itertools
import pprint


dict1 = {
  "A": "a"
}

dict2 = {
  "B": "b",
  "C": "c",
  "D": "d",
  "E": "e"
}

dict3 = {
  "F": "f",
  "G": "g"
}


result = {}
for dicts in ((dict1, dict2), (dict1, dict3), (dict1, dict2, dict3)):
    for t in itertools.product(*(d.items() for d in dicts)):
        k, v = zip(*t)
        result["_".join(k)] = " and ".join(v)

pprint.pprint(result)

Output:

{'A_B': 'a and b',
 'A_B_F': 'a and b and f',
 'A_B_G': 'a and b and g',
 'A_C': 'a and c',
 'A_C_F': 'a and c and f',
 'A_C_G': 'a and c and g',
 'A_D': 'a and d',
 'A_D_F': 'a and d and f',
 'A_D_G': 'a and d and g',
 'A_E': 'a and e',
 'A_E_F': 'a and e and f',
 'A_E_G': 'a and e and g',
 'A_F': 'a and f',
 'A_G': 'a and g'}
Right leg
  • 16,080
  • 7
  • 48
  • 81
  • 1
    is `functools` supposed to be [`itertools`](https://docs.python.org/2/library/itertools.html#itertools.product)? – Ben Jones Apr 11 '19 at 16:23
  • No worries. Now I know about `functools`! – Ben Jones Apr 11 '19 at 16:25
  • 1
    @BenJones Wanna learn about some more magic? Check out `more_itertools` :) – Right leg Apr 11 '19 at 16:27
  • Adding an outer loop of `for dicts in ((dict1, dict2), (dict1, dict3), (dict1, dict2, dict3)):` and making the inner loop `for t in product(*[d.items() for d in dicts]):` would let you produce the result with minimal code repetition. – ShadowRanger Apr 11 '19 at 16:27
  • @ShadowRanger Yep, that's right. I don't really like having to manually list the tuples of `dict*`, but I can't see any elegant alternative... – Right leg Apr 11 '19 at 16:29
  • @Rightleg: Well, I suppose since `dict1` is a part of all the combinations, you could reduce the amount of repetition with `for dicts in ((dict2,), (dict3,), (dict2, dict3)):` followed by `for t in product(dict1.items(), *[d.items() for d in dicts]):`. If there were many more dicts following the same pattern (one required, all others optional), this would let you do `otherdicts = (dict2, dict3, dict4, ..., dictN)`, then make the outer loop `for dicts in chain.from_iterable(combinations(otherdicts, i) for i in range(1, len(otherdicts) + 1)):` and you'd avoid an explicit listing. – ShadowRanger Apr 11 '19 at 16:34
  • But when `N` is `3`, the rigmarole with `chain.from_iterable` and `combinations` and all isn't worth it. – ShadowRanger Apr 11 '19 at 16:35
1

To produce all pairings, you can use two recursive generator functions: one to find the overall combinations of dictionaries, and the other to pair the keys and values:

def pair_dicts(data, c):
   if not data:
     keys, values = zip(*c)
     yield ('_'.join(keys), ' and '.join(values))
   else:
     for i in data[0]:
        yield from pair_dicts(data[1:], c+[i])

def combos(d, c = []):
  if len(c) == len(d):
    yield c
  else:
    if len(c) > 1:
      yield c
    for i in d:
      if all(h != i for h in c):
         yield from combos(d, c+[i])

new_d = [[list(c.items()) for c in i] for i in combos([dict1, dict2, dict3])]
final_result = dict(i for b in new_d for i in pair_dicts(b, []))

Output:

{'A_B': 'a and b', 'A_C': 'a and c', 'A_D': 'a and d', 'A_E': 'a and e', 'A_B_F': 'a and b and f', 'A_B_G': 'a and b and g', 'A_C_F': 'a and c and f', 'A_C_G': 'a and c and g', 'A_D_F': 'a and d and f', 'A_D_G': 'a and d and g', 'A_E_F': 'a and e and f', 'A_E_G': 'a and e and g', 'A_F': 'a and f', 'A_G': 'a and g', 'A_F_B': 'a and f and b', 'A_F_C': 'a and f and c', 'A_F_D': 'a and f and d', 'A_F_E': 'a and f and e', 'A_G_B': 'a and g and b', 'A_G_C': 'a and g and c', 'A_G_D': 'a and g and d', 'A_G_E': 'a and g and e', 'B_A': 'b and a', 'C_A': 'c and a', 'D_A': 'd and a', 'E_A': 'e and a', 'B_A_F': 'b and a and f', 'B_A_G': 'b and a and g', 'C_A_F': 'c and a and f', 'C_A_G': 'c and a and g', 'D_A_F': 'd and a and f', 'D_A_G': 'd and a and g', 'E_A_F': 'e and a and f', 'E_A_G': 'e and a and g', 'B_F': 'b and f', 'B_G': 'b and g', 'C_F': 'c and f', 'C_G': 'c and g', 'D_F': 'd and f', 'D_G': 'd and g', 'E_F': 'e and f', 'E_G': 'e and g', 'B_F_A': 'b and f and a', 'B_G_A': 'b and g and a', 'C_F_A': 'c and f and a', 'C_G_A': 'c and g and a', 'D_F_A': 'd and f and a', 'D_G_A': 'd and g and a', 'E_F_A': 'e and f and a', 'E_G_A': 'e and g and a', 'F_A': 'f and a', 'G_A': 'g and a', 'F_A_B': 'f and a and b', 'F_A_C': 'f and a and c', 'F_A_D': 'f and a and d', 'F_A_E': 'f and a and e', 'G_A_B': 'g and a and b', 'G_A_C': 'g and a and c', 'G_A_D': 'g and a and d', 'G_A_E': 'g and a and e', 'F_B': 'f and b', 'F_C': 'f and c', 'F_D': 'f and d', 'F_E': 'f and e', 'G_B': 'g and b', 'G_C': 'g and c', 'G_D': 'g and d', 'G_E': 'g and e', 'F_B_A': 'f and b and a', 'F_C_A': 'f and c and a', 'F_D_A': 'f and d and a', 'F_E_A': 'f and e and a', 'G_B_A': 'g and b and a', 'G_C_A': 'g and c and a', 'G_D_A': 'g and d and a', 'G_E_A': 'g and e and a'}
Ajax1234
  • 69,937
  • 8
  • 61
  • 102
  • Although it's not an issue here, I'd generally advise against using a list or any other mutable value as a default value, and would rather go for `def combos(d, c=None): if c is None: c = []`. See https://stackoverflow.com/questions/1132941/least-astonishment-and-the-mutable-default-argument – Right leg Apr 11 '19 at 16:59
0

I created a (not so nice) function to do your task with arbitrary number of dictionaries.

(Explanation below)

import itertools as it

dict1 = {
  "A": "a"
}

dict2 = {
  "B": "b", 
  "C": "c",
  "D": "d", 
  "E": "e"
}

dict3 = {
  "F": "f", 
  "G": "g"
}



def custom_dict_product(dictionaries):
    return dict(zip(map("_".join, it.product(*map(dict.keys, dictionaries))), 
                    map(" and ".join, it.product(*map(dict.values, dictionaries)))))

result = custom_dict_product([dict1,dict2])
result.update(custom_dict_product([dict1,dict3]))
result.update(custom_dict_product([dict1,dict2,dict3]))
result
#{'A_B': 'a and b',
# 'A_B_F': 'a and b and f',
# 'A_B_G': 'a and b and g',
# 'A_C': 'a and c',
# 'A_C_F': 'a and c and f',
# 'A_C_G': 'a and c and g',
# 'A_D': 'a and d',
# 'A_D_F': 'a and d and f',
# 'A_D_G': 'a and d and g',
# 'A_E': 'a and e',
# 'A_E_F': 'a and e and f',
# 'A_E_G': 'a and e and g',
# 'A_F': 'a and f',
# 'A_G': 'a and g'}

The function takes the given dictionaries and gets their keys and values, which is done by map(dict.keys, dictionaries))and map(dict.values, dictionaries)). The results of the first call

list(it.product(*map(dict.keys, [dict1,dict2])))
# [('A', 'C'), ('A', 'E'), ('A', 'B'), ('A', 'D')]

The tuples insides this list are then forced to your desired structure with join(and again an map call to do this for every element):

"_".join(('A', 'C'))
# 'A_C'
list(map("_".join, it.product(*map(dict.keys, [dict1,dict2]))))
# ['A_C', 'A_E', 'A_B', 'A_D']

Finally the two resulting lists are transformed to tuples of (keys, values) with the call of zip and handed to the dictionary creation.

Sparky05
  • 4,692
  • 1
  • 10
  • 27
0

Here a dirty, but working, solution that makes use of itertools

from itertools import product, combinations


# create a list and sum dict to be used later
t = [dict1, dict2, dict3] 
k = {}
for d in t:
    k.update(d)


# iterate over "i" order of combinations ("dict1_X" or "dict1_X_Y") and 
# the cartesian product of keys for each combination

results = {}
for i in range(2, 4):
    a = [
        [
            results.update({"_".join(y): " and ".join([k[j] for j in y])})
            for y in product(*x)
        ]
        for x in combinations(t, i) 
        if dict1 in x
    ]

results

Output:

{'A_B': 'a and b',
 'A_B_F': 'a and b and f',
 'A_B_G': 'a and b and g',
 'A_C': 'a and c',
 'A_C_F': 'a and c and f',
 'A_C_G': 'a and c and g',
 'A_D': 'a and d',
 'A_D_F': 'a and d and f',
 'A_D_G': 'a and d and g',
 'A_E': 'a and e',
 'A_E_F': 'a and e and f',
 'A_E_G': 'a and e and g',
 'A_F': 'a and f',
 'A_G': 'a and g'}
Lante Dellarovere
  • 1,838
  • 2
  • 7
  • 10