110 votes

Comment fusionner et sommer deux dictionnaires en Python?

J'ai un dictionnaire ci-dessous, et je veux ajouter à un autre dictionnaire avec des éléments non nécessairement distincts et fusionner ses résultats.

Y a-t-il une fonction intégrée pour cela, ou dois-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.

Lors de la fusion, si deux clés identiques sont rencontrées, les valeurs de ces clés doivent être additionnées au lieu d'être écrasées.

243voto

georg Points 52691

Vous n'avez pas précisé comment vous souhaitez fusionner, donc 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}

96voto

Scott Points 4953

Vous pouvez effectuer +, -, & et | (intersection et union) avec collections.Counter().

Nous pouvons faire ce qui suit (seules les valeurs de compte 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)
[out]:
{'both2': 22, 'only_x': 100, 'both1': 11, 'only_y': 200}

Pour ajouter des valeurs où le résultat peut être zéro ou négatif, utilisez Counter.update() pour l'addition, et Counter.subtract() pour la 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)
[out]:
{'both2': -18, 'only_x': 100, 'both1': 0, 'only_y': 200}

36voto

SCB Points 362

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

J'essayais d'effectuer cette action sur des collections de 2 ou plus de dictionnaires et je voulais voir le temps que cela prenait pour chacun. Comme je voulais le faire sur un nombre quelconque de dictionnaires, j'ai dû modifier un peu certaines des réponses. Si quelqu'un a de meilleures suggestions pour elles, n'hésitez pas à éditer.

Voici ma méthode de test. Je l'ai récemment mise à jour pour inclure des tests avec des dictionnaires BEAUCOUP plus grands, et de nouveau pour inclure les méthodes plus récentes de Havok et Scott:

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 clés aléatoires de 8 lettres
keys = [''.join(random.choice("abcdefghijklmnopqrstuvwxyz") for _ in range(8)) for _ in range(200000)]

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

# 50/50 de chances qu'une valeur soit assignée à chaque dictionnaire, certaines clés seront manquées mais bon
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
from functools import reduce

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):
    # Si vous savez que vous aurez exactement 3 dictionnaires
    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)

# Remarque: Il y a un bug avec la méthode de Scott. Voir ci-dessous pour les détails.
# Scott a inclus une version similaire utilisant des compteurs qui est corrigée
# Voir la méthode de mise à jour scott ci-dessous
def scott_method(tests):
    return dict(sum((Counter(t) for t in tests), Counter()))

def scott_method_nosum(tests):
    # Si vous savez que vous aurez exactement 3 dictionnaires
    return dict(Counter(tests[0]) + Counter(tests[1]) + Counter(tests[2]))

def scott_update_method(tests):
    ret = Counter()
    for test in tests:
        ret.update(test)
    return dict(ret)

def scott_update_method_static(tests):
    # Si vous savez que vous aurez exactement 3 dictionnaires
    xx = Counter(tests[0])
    yy = Counter(tests[1])
    zz = Counter(tests[2])
    xx.update(yy)
    xx.update(zz)
    return dict(xx)

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

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,
    "scott_update_method": scott_update_method, "scott_update_method_static": scott_update_method_static,
    "havok_method": havok_method
}

J'ai également écrit une fonction rapide pour trouver toutes les différences entre les listes. Malheureusement, c'est là que j'ai trouvé le problème dans la méthode de Scott, à savoir, si vous avez des dictionnaires qui totalisent 0, le dictionnaire ne sera pas du tout inclus en raison de la façon dont Counter() se comporte lors de l'ajout.

Configuration de test:

  • MacBook Pro (15 pouces, fin 2016), Intel Core i7 2,9 GHz, 16 Go de RAM LPDDR3 2133 MHz, exécutant macOS Mojave Version 10.14.5
  • Python 3.6.5 via IPython 6.1.0

Enfin, les résultats:

Résultats: Petits Tests

for name, method in methods.items():
    print("Méthode:", name)
    %timeit -n10000 method(small_tests)
#: Méthode: georg_method
#: 7.81 µs ± 321 ns par boucle (moyenne ± écart type de 7 répétitions, 10000 boucles chacune)
#: Méthode: georg_method_nosum
#: 4.6 µs ± 48.8 ns par boucle (moyenne ± écart type de 7 répétitions, 10000 boucles chacune)
#: Méthode: npe_method
#: 3.2 µs ± 24.7 ns par boucle (moyenne ± écart type de 7 répétitions, 10000 boucles chacune)
#: Méthode: scott_method
#: 24.9 µs ± 326 ns par boucle (moyenne ± écart type de 7 répétitions, 10000 boucles chacune)
#: Méthode: scott_method_nosum
#: 18.9 µs ± 64.8 ns par boucle (moyenne ± écart type de 7 répétitions, 10000 boucles chacune)
#: Méthode: scott_update_method
#: 9.1 µs ± 90.7 ns par boucle (moyenne ± écart type de 7 répétitions, 10000 boucles chacune)
#: Méthode: scott_update_method_static
#: 14.4 µs ± 122 ns par boucle (moyenne ± écart type de 7 répétitions, 10000 boucles chacune)
#: Méthode: havok_method
#: 3.09 µs ± 47.9 ns par boucle (moyenne ± écart type de 7 répétitions, 10000 boucles chacune)

Résultats: Grands Tests

Naturellement, je n'ai pas pu exécuter autant de boucles

for name, method in methods.items():
    print("Méthode:", name)
    %timeit -n10 method(large_tests)
#: Méthode: georg_method
#: 347 ms ± 20 ms par boucle (moyenne ± écart type de 7 répétitions, 10 boucles chacune)
#: Méthode: georg_method_nosum
#: 280 ms ± 4.97 ms par boucle (moyenne ± écart type de 7 répétitions, 10 boucles chacune)
#: Méthode: npe_method
#: 119 ms ± 11 ms par boucle (moyenne ± écart type de 7 répétitions, 10 boucles chacune)
#: Méthode: scott_method
#: 324 ms ± 16.8 ms par boucle (moyenne ± écart type de 7 répétitions, 10 boucles chacune)
#: Méthode: scott_method_nosum
#: 289 ms ± 14.3 ms par boucle (moyenne ± écart type de 7 répétitions, 10 boucles chacune)
#: Méthode: scott_update_method
#: 123 ms ± 1.94 ms par boucle (moyenne ± écart type de 7 répétitions, 10 boucles chacune)
#: Méthode: scott_update_method_static
#: 136 ms ± 3.19 ms par boucle (moyenne ± écart type de 7 répétitions, 10 boucles chacune)
#: Méthode: havok_method
#: 103 ms ± 1.31 ms par boucle (moyenne ± écart type de 7 répétitions, 10 boucles chacune)

Conclusion

╔═══════════════════════════╦═══════╦═════════════════════════════╗
║                           ║       ║ Meilleur Temps par Boucle    ║
║         Algorithme        ║  Par  ╠══════════════╦══════════════╣
║                           ║       ║  small_tests ║  large_tests ║
╠═══════════════════════════╬═══════╬══════════════╬══════════════╣
║ functools reduce          ║ Havok ║       3.1 µs ║   103,000 µs ║
║ defaultdict sum           ║ NPE   ║       3.2 µs ║   119,000 µs ║
║ Boucle Counter().update   ║ Scott ║       9.1 µs ║   123,000 µs ║
║ Counter().update statique ║ Scott ║      14.4 µs ║   136,000 µs ║
║ set unions sans sum()     ║ georg ║       4.6 µs ║   280,000 µs ║
║ set unions avec sum()     ║ georg ║       7.8 µs ║   347,000 µs ║
║ Counter() sans sum()      ║ Scott ║      18.9 µs ║   289,000 µs ║
║ Counter() avec sum()      ║ Scott ║      24.9 µs ║   324,000 µs ║
╚═══════════════════════════╩═══════╩══════════════╩══════════════╝

Important. Les résultats peuvent varier selon votre machine.

23voto

NPE Points 169956

Vous pourriez utiliser defaultdict pour cela:

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}

23voto

Havok Points 1502

Autre option en utilisant une fonction de réduction. Cela permet de fusionner 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, pythonique.
  • Sans schéma, tant que toutes les clés sont "additionnables".
  • Complexité temporelle en O(n) et complexité mémoire en O(1).

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X