Слияние и сумма двух словарей
У меня есть словарь ниже, и я хочу добавить в другой словарь не обязательно отдельные элементы и объединить его результаты. Есть ли встроенная функция для этого, или мне нужно будет сделать свою собственную?
{
'6d6e7bf221ae24e07ab90bba4452267b05db7824cd3fd1ea94b2c9a8': 6,
'7c4a462a6ed4a3070b6d78d97c90ac230330603d24a58cafa79caf42': 7,
'9c37bdc9f4750dd7ee2b558d6c06400c921f4d74aabd02ed5b4ddb38': 9,
'd3abb28d5776aef6b728920b5d7ff86fa3a71521a06538d2ad59375a': 15,
'2ca9e1f9cbcd76a5ce1772f9b59995fd32cbcffa8a3b01b5c9c8afc2': 11
}
Количество элементов в словаре также неизвестно.
Если при слиянии рассматриваются два идентичных ключа, значения этих ключей должны суммироваться, а не перезаписываться.
Ответы
Ответ 1
Вы не сказали, как именно вы хотите слиться, так что сделайте выбор:
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) }
Результаты:
{'both2': 22, 'only_x': 100, 'both1': 11}
{'both2': 22, 'both1': 11}
{'only_y': 200, 'both2': 22, 'both1': 11, 'only_x': 100}
Ответ 2
Вы можете выполнять +
, -
, &
и |
(пересечение и объединение) на collections.Counter()
.
Итак, мы можем сделать следующее (Примечание: в словаре остаются только положительные значения счета):
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}
Чтобы добавить значения, в которых результат может быть нулевым или отрицательным, используйте Counter.update()
для добавления и Counter.subtract()
для вычитания:
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}
Ответ 3
Вы можете использовать defaultdict
для этого:
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))
Это создает
{'both1': 11, 'both2': 22, 'only_x': 100, 'only_y': 200}
Ответ 4
Дополнительные заметки основаны на ответах Георга, NPE, Скотта и Хавока.
Я пытался выполнить это действие для коллекций из двух или более словарей и мне было интересно посмотреть, сколько времени потребуется для каждого из них. Поскольку я хотел сделать это в любом количестве словарей, мне пришлось немного изменить некоторые ответы. Если у кого-то есть лучшие предложения для них, не стесняйтесь редактировать.
Вот мой метод испытаний. Недавно я обновил его, чтобы включить тесты с НАМНОГО более крупными словарями и снова включить новые методы Havok и Scott:
Сначала я использовал следующие данные:
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
Теперь каждый из методов:
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):
# 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 method. See below for details.
# Scott included a similar version using counters that is fixed
# See the scott_update_method below
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]))
def scott_update_method(tests):
ret = Counter()
for test in tests:
ret.update(test)
return dict(ret)
def scott_update_method_static(tests):
# If you know you will have exactly 3 dicts
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
}
Я также написал быструю функцию поиска различий между списками. К сожалению, когда я нашел проблему в методе Скотта, а именно, если у вас есть словари с общим значением 0, словарь вообще не будет включен из-за того, как Counter()
ведет себя при добавлении.
Испытательная установка:
- MacBook Pro (15 дюймов, конец 2016 г.), Intel Core i7 с тактовой частотой 2,9 ГГц, 16 ГБ ОЗУ LPDDR3 с тактовой частотой 2133 МГц и MacOS Mojave версии 10.14.5
- Python 3.6.5 через IPython 6.1.0
Наконец, результаты:
Результаты: небольшие тесты
for name, method in methods.items():
print("Method:", name)
%timeit -n10000 method(small_tests)
#: Method: georg_method
#: 7.81 µs ± 321 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
#: Method: georg_method_nosum
#: 4.6 µs ± 48.8 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
#: Method: npe_method
#: 3.2 µs ± 24.7 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
#: Method: scott_method
#: 24.9 µs ± 326 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
#: Method: scott_method_nosum
#: 18.9 µs ± 64.8 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
#: Method: scott_update_method
#: 9.1 µs ± 90.7 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
#: Method: scott_update_method_static
#: 14.4 µs ± 122 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
#: Method: havok_method
#: 3.09 µs ± 47.9 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
Результаты: Большие Тесты
Естественно, не мог бегать где-то рядом столько петель
for name, method in methods.items():
print("Method:", name)
%timeit -n10 method(large_tests)
Заключение
╔═══════════════════════════╦═══════╦═════════════════════════════╗
║ ║ ║ Best of Time Per Loop ║
║ Algorithm ║ By ╠══════════════╦══════════════╣
║ ║ ║ small_tests ║ large_tests ║
╠═══════════════════════════╬═══════╬══════════════╬══════════════╣
║ fuctools reduce ║ Havok ║ 3.1 µs ║ 103,000 µs ║
║ defaultdict sum ║ NPE ║ 3.2 µs ║ 119,000 µs ║
║ Counter().update loop ║ Scott ║ 9.1 µs ║ 123,000 µs ║
║ Counter().update static ║ Scott ║ 14.4 µs ║ 136,000 µs ║
║ set unions without sum() ║ georg ║ 4.6 µs ║ 280,000 µs ║
║ set unions with sum() ║ georg ║ 7.8 µs ║ 347,000 µs ║
║ Counter() without sum() ║ Scott ║ 18.9 µs ║ 289,000 µs ║
║ Counter() with sum() ║ Scott ║ 24.9 µs ║ 324,000 µs ║
╚═══════════════════════════╩═══════╩══════════════╩══════════════╝
Важный. YMMV.
Ответ 5
Другие опции, использующие функцию уменьшения. Это позволяет суммировать произвольный набор словарей:
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)
Исполнение:
{'a': 45, 'b': 45, 'c': 3}
Преимущества:
- Простой, понятный, Pythonic.
- Без схемы, поскольку все ключи "подлежат заимствованию".
- O (n) временная сложность и сложность памяти O (1).
Ответ 6
Я подозреваю, что вы ищете dict
update
метод:
>>> d1 = {1:2,3:4}
>>> d2 = {5:6,7:8}
>>> d1.update(d2)
>>> d1
{1: 2, 3: 4, 5: 6, 7: 8}
Ответ 7
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(),
)
Существует также довольно простая замена dict.update()
:
merged = dict(d1, **d2)
Ответ 8
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}
Это перегрузка оператора. Используя __add__
, мы определили, как использовать оператор +
для нашего dict_merge
, который наследуется от встроенного питона dict
. Вы можете пойти дальше и сделать его более гибким, используя аналогичный способ определения других операторов в этом же классе, например. *
с __mul__
для умножения или /
с __div__
для деления или даже %
с __mod__
по модулю и заменой +
in self[key] + other[key]
на соответствующий оператор, если вы когда-либо находить нужное такое слияние.
Я тестировал это только как без других операторов, но я не предвижу проблемы с другими операторами. Просто учитесь, пытаясь.
Ответ 9
Если вы хотите создать новый dict
как |
, используйте:
>>> dict({'a': 1,'c': 2}, **{'c': 1})
{'a': 1, 'c': 1}