web-dev-qa-db-fra.com

Fusion et somme de deux dictionnaires

J'ai un dictionnaire ci-dessous, et je veux ajouter à un autre dictionnaire avec des éléments pas nécessairement distincts et fusionner ses résultats. Y at-il une fonction intégrée pour cela, ou devrais-je créer la mienne?

{
  '6d6e7bf221ae24e07ab90bba4452267b05db7824cd3fd1ea94b2c9a8': 6,
  '7c4a462a6ed4a3070b6d78d97c90ac230330603d24a58cafa79caf42': 7,
  '9c37bdc9f4750dd7ee2b558d6c06400c921f4d74aabd02ed5b4ddb38': 9,
  'd3abb28d5776aef6b728920b5d7ff86fa3a71521a06538d2ad59375a': 15,
  '2ca9e1f9cbcd76a5ce1772f9b59995fd32cbcffa8a3b01b5c9c8afc2': 11
}

Le nombre d'éléments dans le dictionnaire est également inconnu.

Lorsque la fusion considère deux clés identiques, les valeurs de ces clés doivent être additionnées au lieu d'être écrasées.

37
badc0re

Vous n'avez pas dit à quel point vous voulez fusionner, alors faites votre choix:

x = {'both1':1, 'both2':2, 'only_x': 100 }
y = {'both1':10, 'both2': 20, 'only_y':200 }

print { k: x.get(k, 0) + y.get(k, 0) for k in set(x) }
print { k: x.get(k, 0) + y.get(k, 0) for k in set(x) & set(y) }
print { k: x.get(k, 0) + y.get(k, 0) for k in set(x) | set(y) }

Résultats:

{'both2': 22, 'only_x': 100, 'both1': 11}
{'both2': 22, 'both1': 11}
{'only_y': 200, 'both2': 22, 'both1': 11, 'only_x': 100}
111
georg

Vous pouvez exécuter +, -, & et | (intersection et union) sur collections.Counter() .

Nous pouvons donc faire ce qui suit (Remarque: seules les valeurs de comptage positives resteront dans le dictionnaire):

from collections import Counter

x = {'both1':1, 'both2':2, 'only_x': 100 }
y = {'both1':10, 'both2': 20, 'only_y':200 }

z = dict(Counter(x)+Counter(y))

print(z) # {'both2': 22, 'only_x': 100, 'both1': 11, 'only_y': 200}

Pour traiter les valeurs ajoutées où le résultat peut être nul ou négatif, utilisez Counter.update() pour l'addition et Counter.subtract() pour soustraction:

x = {'both1':0, 'both2':2, 'only_x': 100 }
y = {'both1':0, 'both2': -20, 'only_y':200 }
xx = Counter(x)
yy = Counter(y)
xx.update(yy)
dict(xx) # {'both2': -18, 'only_x': 100, 'both1': 0, 'only_y': 200}
25
Scott

Vous pouvez utiliser defaultdict pour ceci:

from collections import defaultdict

def dsum(*dicts):
    ret = defaultdict(int)
    for d in dicts:
        for k, v in d.items():
            ret[k] += v
    return dict(ret)

x = {'both1':1, 'both2':2, 'only_x': 100 }
y = {'both1':10, 'both2': 20, 'only_y':200 }

print(dsum(x, y))

Cela produit

{'both1': 11, 'both2': 22, 'only_x': 100, 'only_y': 200}
17
NPE

Notes complémentaires basées sur les réponses de georg , NPE et Scott .

J'essayais de réaliser cette action sur des collections de 2 ou plus dictionnaires et je voulais voir le temps que cela prenait. Parce que je voulais faire cela sur plusieurs dictionnaires, je devais changer un peu certaines réponses. Si quelqu'un a de meilleures suggestions pour eux, n'hésitez pas à le modifier.

Voici ma méthode de test. Je l'ai récemment mis à jour pour inclure des tests avec BEAUCOUP de dictionnaires:

Tout d'abord j'ai utilisé les données suivantes:

import random

x = {'xy1': 1, 'xy2': 2, 'xyz': 3, 'only_x': 100}
y = {'xy1': 10, 'xy2': 20, 'xyz': 30, 'only_y': 200}
z = {'xyz': 300, 'only_z': 300}

small_tests = [x, y, z]

# 200,000 random 8 letter keys
keys = [''.join(random.choice("abcdefghijklmnopqrstuvwxyz") for _ in range(8)) for _ in range(200000)]

a, b, c = {}, {}, {}

# 50/50 chance of a value being assigned to each dictionary, some keys will be missed but meh
for key in keys:
    if random.getrandbits(1):
        a[key] = random.randint(0, 1000)
    if random.getrandbits(1):
        b[key] = random.randint(0, 1000)
    if random.getrandbits(1):
        c[key] = random.randint(0, 1000)

large_tests = [a, b, c]

print("a:", len(a), "b:", len(b), "c:", len(c))
#: a: 100069 b: 100385 c: 99989

Maintenant chacune des méthodes:

from collections import defaultdict, Counter

def georg_method(tests):
    return {k: sum(t.get(k, 0) for t in tests) for k in set.union(*[set(t) for t in tests])}

def georg_method_nosum(tests):
    # If you know you will have exactly 3 dicts
    return {k: tests[0].get(k, 0) + tests[1].get(k, 0) + tests[2].get(k, 0) for k in set.union(*[set(t) for t in tests])}

def npe_method(tests):
    ret = defaultdict(int)
    for d in tests:
        for k, v in d.items():
            ret[k] += v
    return dict(ret)

# Note: There is a bug with scott's method. See below for details.
def scott_method(tests):
    return dict(sum((Counter(t) for t in tests), Counter()))

def scott_method_nosum(tests):
    # If you know you will have exactly 3 dicts
    return dict(Counter(tests[0]) + Counter(tests[1]) + Counter(tests[2]))

methods = {"georg_method": georg_method, "georg_method_nosum": georg_method_nosum,
           "npe_method": npe_method,
           "scott_method": scott_method, "scott_method_nosum": scott_method_nosum}

J'ai aussi écrit une fonction rapide pour trouver les différences entre les listes. Malheureusement, c'est là que j'ai trouvé le problème dans la méthode de Scott, à savoir que si vous avez des dictionnaires dont le total est égal à 0, le dictionnaire ne sera pas inclus du tout à cause du comportement de Counter() lors de l'ajout.

Enfin, les résultats:

Résultats: petits tests

for name, method in methods.items():
    print("Method:", name)
    %timeit -n10000 method(small_tests)
#: Method: npe_method
#: 10000 loops, best of 3: 5.16 µs per loop
#: Method: georg_method_nosum
#: 10000 loops, best of 3: 8.11 µs per loop
#: Method: georg_method
#: 10000 loops, best of 3: 11.8 µs per loop
#: Method: scott_method_nosum
#: 10000 loops, best of 3: 42.4 µs per loop
#: Method: scott_method
#: 10000 loops, best of 3: 65.3 µs per loop

Résultats: grands tests

Naturellement, je ne pouvais pas courir autant de boucles

for name, method in methods.items():
    print("Method:", name)
    %timeit -n10 method(large_tests)
#: Method: npe_method
#: 10 loops, best of 3: 227 ms per loop
#: Method: georg_method_nosum
#: 10 loops, best of 3: 327 ms per loop
#: Method: georg_method
#: 10 loops, best of 3: 455 ms per loop
#: Method: scott_method_nosum
#: 10 loops, best of 3: 510 ms per loop
#: Method: scott_method
#: 10 loops, best of 3: 600 ms per loop

Conclusion

╔═══════════════════════════╦═══════╦═════════════════════════════╗
║                           ║       ║   Best of 3 Time Per Loop   ║
║         Algorithm         ║  By   ╠══════════════╦══════════════╣
║                           ║       ║  small_tests ║  large_tests ║
╠═══════════════════════════╬═══════╬══════════════╬══════════════╣
║ defaultdict sum           ║ NPE   ║      5.16 µs ║   227,000 µs ║
║ set unions without sum()  ║ georg ║      8.11 µs ║   327,000 µs ║
║ set unions with sum()     ║       ║      11.8 µs ║   455,000 µs ║
║ Counter() without sum()   ║ Scott ║      42.4 µs ║   510,000 µs ║
║ Counter() with sum()      ║       ║      65.3 µs ║   600,000 µs ║
╚═══════════════════════════╩═══════╩══════════════╩══════════════╝

Important. YMMV.

10
SCB

Une autre option utilisant une fonction de réduction. Cela permet de résumer une collection arbitraire de dictionnaires:

from functools import reduce

collection = [
    {'a': 1, 'b': 1},
    {'a': 2, 'b': 2},
    {'a': 3, 'b': 3},
    {'a': 4, 'b': 4, 'c': 1},
    {'a': 5, 'b': 5, 'c': 1},
    {'a': 6, 'b': 6, 'c': 1},
    {'a': 7, 'b': 7},
    {'a': 8, 'b': 8},
    {'a': 9, 'b': 9},
]


def reducer(accumulator, element):
    for key, value in element.items():
        accumulator[key] = accumulator.get(key, 0) + value
    return accumulator


total = reduce(reducer, collection, {})


assert total['a'] == sum(d.get('a', 0) for d in collection)
assert total['b'] == sum(d.get('b', 0) for d in collection)
assert total['c'] == sum(d.get('c', 0) for d in collection)

print(total)

Exécution:

{'a': 45, 'b': 45, 'c': 3}

Avantages:

  • Simple, clair, Pythonic.
  • Schema-less, tant que toutes les clés sont "sumables".
  • O (n) complexité temporelle et O(1) complexité de la mémoire.
3
Havok
d1 = {'apples': 2, 'banana': 1}
d2 = {'apples': 3, 'banana': 2}
merged = reduce(
    lambda d, i: (
        d.update(((i[0], d.get(i[0], 0) + i[1]),)) or d
    ),
    d2.iteritems(),
    d1.copy(),
)

Il y a aussi un remplacement assez simple de dict.update():

merged = dict(d1, **d2)
1
renskiy

Je suspecte que vous recherchiez la méthode dict_ update :

>>> d1 = {1:2,3:4}
>>> d2 = {5:6,7:8}
>>> d1.update(d2)
>>> d1
{1: 2, 3: 4, 5: 6, 7: 8}
1
zigg
class dict_merge(dict):
def __add__(self, other):
    result = dict_merge({})
    for key in self.keys():
        if key in other.keys():
            result[key] = self[key] + other[key]
        else:
            result[key] = self[key]
    for key in other.keys():
        if key in self.keys():
            pass
        else:
            result[key] = other[key]
    return result


a = dict_merge({"a":2, "b":3, "d":4})
b = dict_merge({"a":1, "b":2})
c = dict_merge({"a":5, "b":6, "c":5})
d = dict_merge({"a":8, "b":6, "e":5})

print((a + b + c +d))


>>> {'a': 16, 'b': 17, 'd': 4, 'c': 5, 'e': 5}

C'est la surcharge de l'opérateur. En utilisant __add__, nous avons défini comment utiliser l'opérateur + pour notre dict_merge qui hérite du python intégré dict. Vous pouvez aller de l'avant et rendre plus flexible en utilisant une méthode similaire pour définir d'autres opérateurs dans cette même classe, par exemple. * avec __mul__ pour multiplier, ou / avec __div__ pour diviser, ou même % avec __mod__ pour modulo, et remplacer le + dans self[key] + other[key] par l'opérateur correspondant, si jamais vous aviez besoin de cette fusion . Je ne l'ai testé que comme c'est sans autres opérateurs mais je ne prévois pas de problème avec d'autres opérateurs. Juste apprendre en essayant.

1
John Mutuma

Si vous voulez créer une nouvelle dict en tant que |, utilisez:

>>> dict({'a': 1,'c': 2}, **{'c': 1})
{'a': 1, 'c': 1}
0
Bartosz Foder