Czy istnieje jakiś pythonowy sposób łączenia dwóch nagrań (dodawanie wartości dla kluczy pojawiających się w obu)?

477

Na przykład mam dwa dyktanda:

Dict A: {'a': 1, 'b': 2, 'c': 3}
Dict B: {'b': 3, 'c': 4, 'd': 5}

Potrzebuję pytonowego sposobu „łączenia” dwóch dykt, tak aby wynik był następujący:

{'a': 1, 'b': 5, 'c': 7, 'd': 5}

To znaczy: jeśli klucz pojawia się w obu nagraniach, dodaj ich wartości, jeśli pojawia się tylko w jednym nagraniu, zachowaj jego wartość.

Derrick Zhang
źródło

Odpowiedzi:

835

Użyj collections.Counter:

>>> from collections import Counter
>>> A = Counter({'a':1, 'b':2, 'c':3})
>>> B = Counter({'b':3, 'c':4, 'd':5})
>>> A + B
Counter({'c': 7, 'b': 5, 'd': 5, 'a': 1})

Liczniki są w zasadzie podklasą dict, więc nadal możesz robić z nimi wszystko, co normalnie zrobiłbyś z tym typem, na przykład powtarzać klucze i wartości.

Martijn Pieters
źródło
4
Co jest wielu licznikami do scalenia w ten sposób? sum(counters)nie działa niestety.
Dr Jan-Philip Gehrcke
27
@ Jan-PhilipGehrcke: Podaj sum()wartość początkową za pomocą sum(counters, Counter()).
Martijn Pieters
5
Dzięki. Jednak na tę metodę ma wpływ tworzenie obiektów pośrednich, ponieważ sumowanie ciągów jest, prawda?
Dr Jan-Philip Gehrcke
6
@ Jan-PhilipGehrcke: Inną opcją jest użycie pętli i +=sumowanie w miejscu. res = counters[0], A następnie for c in counters[1:]: res += c.
Martijn Pieters
3
Lubię to podejście! Jeśli ktoś lubi trzymać rzeczy blisko słowników przetwórczych, można również użyć update()zamiast +=: for c in counters[1:]: res.update(c).
Dr Jan-Philip Gehrcke
119

Bardziej ogólne rozwiązanie, które działa również dla wartości nienumerycznych:

a = {'a': 'foo', 'b':'bar', 'c': 'baz'}
b = {'a': 'spam', 'c':'ham', 'x': 'blah'}

r = dict(a.items() + b.items() +
    [(k, a[k] + b[k]) for k in set(b) & set(a)])

lub nawet bardziej ogólny:

def combine_dicts(a, b, op=operator.add):
    return dict(a.items() + b.items() +
        [(k, op(a[k], b[k])) for k in set(b) & set(a)])

Na przykład:

>>> a = {'a': 2, 'b':3, 'c':4}
>>> b = {'a': 5, 'c':6, 'x':7}

>>> import operator
>>> print combine_dicts(a, b, operator.mul)
{'a': 10, 'x': 7, 'c': 24, 'b': 3}
Georg
źródło
27
Możesz także użyć for k in b.viewkeys() & a.viewkeys(), używając Pythona 2.7 , i pominąć tworzenie zestawów.
Martijn Pieters
Dlaczego set(a)zwraca zestaw kluczy zamiast zestawu krotek? Jakie jest tego uzasadnienie?
Sarsaparilla,
1
@HaiPhan: ponieważ dyktuje iterację kluczy, a nie par kv. CF list({..}), for k in {...}itp
Georg
2
@Craicerjack: tak, zwykłem operator.mulwyjaśniać, że ten kod jest ogólny i nie ogranicza się do dodawania liczb.
georg
6
Czy możesz dodać opcję zgodną z Python 3? {**a, **b, **{k: op(a[k], b[k]) for k in a.keys() & b}}powinien działać w Python 3.5+.
vaultah
66
>>> A = {'a':1, 'b':2, 'c':3}
>>> B = {'b':3, 'c':4, 'd':5}
>>> c = {x: A.get(x, 0) + B.get(x, 0) for x in set(A).union(B)}
>>> print(c)

{'a': 1, 'c': 7, 'b': 5, 'd': 5}
Ashwini Chaudhary
źródło
1
Czy używanie nie for x in set(itertools.chain(A, B))byłoby bardziej logiczne? Czy używanie zestawu na dykcie jest trochę nonsensowne, ponieważ klucze są już unikalne? Wiem, że to tylko inny sposób na zdobycie zestawu kluczy, ale uważam, że jest to bardziej mylące niż używanie itertools.chain(co oznacza, że ​​wiesz, co itertools.chainrobi)
jeromej
45

Wprowadzenie: Istnieją (prawdopodobnie) najlepsze rozwiązania. Ale musisz to wiedzieć i pamiętać o tym, a czasem musisz mieć nadzieję, że twoja wersja Pythona nie jest zbyt stara lub jakikolwiek problem.

Są też najbardziej „hackerskie” rozwiązania. Są świetne i krótkie, ale czasem trudno je zrozumieć, przeczytać i zapamiętać.

Istnieje jednak alternatywa polegająca na ponownym wymyśleniu koła. - Po co wymyślać koło? - Zasadniczo dlatego, że jest to naprawdę dobry sposób na naukę (a czasami tylko dlatego, że już istniejące narzędzie nie robi dokładnie tego, co chcesz i / lub tak, jak chcesz) i najłatwiejszy, jeśli nie wiesz lub nie pamiętam idealnego narzędzia dla twojego problemu.

Tak , że proponuje nowo koło z Countergrupy z collectionsmodułem (co najmniej częściowo)

class MyDict(dict):
    def __add__(self, oth):
        r = self.copy()

        try:
            for key, val in oth.items():
                if key in r:
                    r[key] += val  # You can custom it here
                else:
                    r[key] = val
        except AttributeError:  # In case oth isn't a dict
            return NotImplemented  # The convention when a case isn't handled

        return r

a = MyDict({'a':1, 'b':2, 'c':3})
b = MyDict({'b':3, 'c':4, 'd':5})

print(a+b)  # Output {'a':1, 'b': 5, 'c': 7, 'd': 5}

Prawdopodobnie istniałby inny sposób, aby to zaimplementować i istnieją już narzędzia do tego, ale zawsze miło jest wyobrazić sobie, jak rzeczy w zasadzie będą działać.

jeromej
źródło
3
Przyjemny dla tych z nas, którzy wciąż są w wersji 2.6
Brian B
13
myDict = {}
for k in itertools.chain(A.keys(), B.keys()):
    myDict[k] = A.get(k, 0)+B.get(k, 0)

źródło
13

Ten bez dodatkowych importów!

Ich jest pythonowym standardem o nazwie EAFP (łatwiej prosić o przebaczenie niż pozwolenie). Poniższy kod oparty jest na tym standardzie Pythona .

# The A and B dictionaries
A = {'a': 1, 'b': 2, 'c': 3}
B = {'b': 3, 'c': 4, 'd': 5}

# The final dictionary. Will contain the final outputs.
newdict = {}

# Make sure every key of A and B get into the final dictionary 'newdict'.
newdict.update(A)
newdict.update(B)

# Iterate through each key of A.
for i in A.keys():

    # If same key exist on B, its values from A and B will add together and
    # get included in the final dictionary 'newdict'.
    try:
        addition = A[i] + B[i]
        newdict[i] = addition

    # If current key does not exist in dictionary B, it will give a KeyError,
    # catch it and continue looping.
    except KeyError:
        continue

EDYCJA: dzięki jerzyk za sugestie ulepszenia.

Devesh Saini
źródło
5
Algorytm n ^ 2 będzie znacznie wolniejszy niż metoda Counter
Joop
@DeveshSaini lepiej, ale wciąż nie optymalnie :) np .: czy naprawdę potrzebujesz sortowania? a następnie, dlaczego dwie pętle? masz już wszystkie klucze w nowym wydaniu, tylko małe wskazówki do optymalizacji
Jerzyk
Algorytm n ^ 1 został umieszczony zamiast poprzedniego algorytmu n ^ 2 @Joop
Devesh Saini
11

Zdecydowanie sumowanie Counter()s jest najbardziej pytoniczną drogą w takich przypadkach, ale tylko wtedy, gdy daje dodatnią wartość . Oto przykład i, jak widać, nie ma crezultatu po zanegowaniu cwartości w Bsłowniku.

In [1]: from collections import Counter

In [2]: A = Counter({'a':1, 'b':2, 'c':3})

In [3]: B = Counter({'b':3, 'c':-4, 'd':5})

In [4]: A + B
Out[4]: Counter({'d': 5, 'b': 5, 'a': 1})

Wynika to z faktu, że Counters zostały zaprojektowane przede wszystkim do pracy z dodatnimi liczbami całkowitymi do reprezentowania liczby zliczeń (liczba ujemna jest bez znaczenia). Aby jednak pomóc w tych przypadkach użycia, Python dokumentuje minimalne ograniczenia zakresu i typów w następujący sposób:

  • Sama klasa Counter jest podklasą słownika bez ograniczeń dotyczących jej kluczy i wartości. Wartości mają być liczbami reprezentującymi liczby, ale można przechowywać wszystko w polu wartości.
  • The most_common()Metoda wymaga jedynie, że wartości będzie można zamówić.
  • W przypadku operacji lokalnych, takich jak c[key] += 1, typ wartości wymaga jedynie obsługi dodawania i odejmowania. Ułamki, zmiennoprzecinkowe i dziesiętne działałyby i obsługiwane są wartości ujemne. To samo dotyczy również update()isubtract() które pozwalają na wartości ujemne i zerowe zarówno dla wejść, jak i wyjść.
  • Metody wielosetowe są zaprojektowane tylko dla przypadków użycia z wartościami dodatnimi. Wejścia mogą być ujemne lub zerowe, ale tworzone są tylko wyjścia o wartościach dodatnich. Nie ma ograniczeń typu, ale typ wartości musi obsługiwać dodawanie, odejmowanie i porównywanie.
  • elements()Metoda wymaga Integer liczy. Ignoruje zera i liczby ujemne.

Aby ominąć ten problem po zsumowaniu licznika, możesz użyć go Counter.update, aby uzyskać pożądany efekt. Działa jak, dict.update()ale dodaje liczby zamiast ich zastępowania.

In [24]: A.update(B)

In [25]: A
Out[25]: Counter({'d': 5, 'b': 5, 'a': 1, 'c': -1})
Kasramvd
źródło
10
import itertools
import collections

dictA = {'a':1, 'b':2, 'c':3}
dictB = {'b':3, 'c':4, 'd':5}

new_dict = collections.defaultdict(int)
# use dict.items() instead of dict.iteritems() for Python3
for k, v in itertools.chain(dictA.iteritems(), dictB.iteritems()):
    new_dict[k] += v

print dict(new_dict)

# OUTPUT
{'a': 1, 'c': 7, 'b': 5, 'd': 5}

LUB

Alternatywnie możesz użyć Counter, jak wspomniano powyżej w @Martijn.

Adeel
źródło
7

Aby uzyskać bardziej ogólny i rozszerzalny sposób, sprawdź scaledict . Wykorzystuje singledispatchi może łączyć wartości na podstawie swoich typów.

Przykład:

from mergedict import MergeDict

class SumDict(MergeDict):
    @MergeDict.dispatch(int)
    def merge_int(this, other):
        return this + other

d2 = SumDict({'a': 1, 'b': 'one'})
d2.merge({'a':2, 'b': 'two'})

assert d2 == {'a': 3, 'b': 'two'}
schettino72
źródło
5

Z Python 3.5: scalanie i sumowanie

Dzięki @tokeinizer_fsj, który powiedział mi w komentarzu, że nie do końca rozumiem znaczenie pytania (myślałem, że dodanie oznacza po prostu dodanie kluczy, które ostatecznie różnią się w dwóch słownikach, a zamiast tego miałem na myśli wspólne wartości klucza należy zsumować). Dodałem więc tę pętlę przed scaleniem, aby drugi słownik zawierał sumę wspólnych kluczy. Ostatni słownik będzie tym, którego wartości będą trwać w nowym słowniku, który jest wynikiem połączenia dwóch, więc myślę, że problem został rozwiązany. Rozwiązanie jest poprawne z Pythona 3.5 i kolejnych wersji.

a = {
    "a": 1,
    "b": 2,
    "c": 3
}

b = {
    "a": 2,
    "b": 3,
    "d": 5
}

# Python 3.5

for key in b:
    if key in a:
        b[key] = b[key] + a[key]

c = {**a, **b}
print(c)

>>> c
{'a': 3, 'b': 5, 'c': 3, 'd': 5}

Kod wielokrotnego użytku

a = {'a': 1, 'b': 2, 'c': 3}
b = {'b': 3, 'c': 4, 'd': 5}


def mergsum(a, b):
    for k in b:
        if k in a:
            b[k] = b[k] + a[k]
    c = {**a, **b}
    return c


print(mergsum(a, b))
Giovanni G. PY
źródło
W ten sposób scalania słowników nie dodaje się wartości wspólnych kluczy. W pytaniu pożądana wartość klucza bto 5(2 + 3), ale twoja metoda powraca 3.
tokenizer_fsj
4

Dodatkowo należy pamiętać, że a.update( b )jest 2x szybszy niża + b

from collections import Counter
a = Counter({'menu': 20, 'good': 15, 'happy': 10, 'bar': 5})
b = Counter({'menu': 1, 'good': 1, 'bar': 3})

%timeit a + b;
## 100000 loops, best of 3: 8.62 µs per loop
## The slowest run took 4.04 times longer than the fastest. This could mean that an intermediate result is being cached.

%timeit a.update(b)
## 100000 loops, best of 3: 4.51 µs per loop
powinieneś zobaczyć
źródło
2
def merge_with(f, xs, ys):
    xs = a_copy_of(xs) # dict(xs), maybe generalizable?
    for (y, v) in ys.iteritems():
        xs[y] = v if y not in xs else f(xs[x], v)

merge_with((lambda x, y: x + y), A, B)

Możesz łatwo to uogólnić:

def merge_dicts(f, *dicts):
    result = {}
    for d in dicts:
        for (k, v) in d.iteritems():
            result[k] = v if k not in result else f(result[k], v)

Może to zająć dowolną liczbę dykt.

Jonas Kölker
źródło
2

Jest to proste rozwiązanie do łączenia dwóch słowników, w których +=można zastosować wartości, musi iterować słownik tylko raz

a = {'a':1, 'b':2, 'c':3}

dicts = [{'b':3, 'c':4, 'd':5},
         {'c':9, 'a':9, 'd':9}]

def merge_dicts(merged,mergedfrom):
    for k,v in mergedfrom.items():
        if k in merged:
            merged[k] += v
        else:
            merged[k] = v
    return merged

for dct in dicts:
    a = merge_dicts(a,dct)
print (a)
#{'c': 16, 'b': 5, 'd': 14, 'a': 10}
ragardner
źródło
1

To rozwiązanie jest łatwe w użyciu, jest używane jako zwykły słownik, ale można użyć funkcji sumowania.

class SumDict(dict):
    def __add__(self, y):
        return {x: self.get(x, 0) + y.get(x, 0) for x in set(self).union(y)}

A = SumDict({'a': 1, 'c': 2})
B = SumDict({'b': 3, 'c': 4})  # Also works: B = {'b': 3, 'c': 4}
print(A + B)  # OUTPUT {'a': 1, 'b': 3, 'c': 6}
Ignacio Villela
źródło
1

Co powiesz na:

def dict_merge_and_sum( d1, d2 ):
    ret = d1
    ret.update({ k:v + d2[k] for k,v in d1.items() if k in d2 })
    ret.update({ k:v for k,v in d2.items() if k not in d1 })
    return ret

A = {'a': 1, 'b': 2, 'c': 3}
B = {'b': 3, 'c': 4, 'd': 5}

print( dict_merge_and_sum( A, B ) )

Wynik:

{'d': 5, 'a': 1, 'c': 7, 'b': 5}
Lacobus
źródło
0

Powyższe rozwiązania są idealne dla scenariusza, w którym masz niewielką liczbę Counters. Jeśli masz ich dużą listę, coś takiego jest o wiele ładniejsze:

from collections import Counter

A = Counter({'a':1, 'b':2, 'c':3})
B = Counter({'b':3, 'c':4, 'd':5}) 
C = Counter({'a': 5, 'e':3})
list_of_counts = [A, B, C]

total = sum(list_of_counts, Counter())

print(total)
# Counter({'c': 7, 'a': 6, 'b': 5, 'd': 5, 'e': 3})

Powyższe rozwiązanie zasadniczo podsumowuje Counters poprzez:

total = Counter()
for count in list_of_counts:
    total += count
print(total)
# Counter({'c': 7, 'a': 6, 'b': 5, 'd': 5, 'e': 3})

Robi to samo, ale myślę, że zawsze pomaga zobaczyć, co skutecznie robi pod spodem.

Michael Hall
źródło
0

Scalenie trzech dykt a, b, c w jednej linii bez żadnych innych modułów lub bibliotek

Jeśli mamy trzy dyktanda

a = {"a":9}
b = {"b":7}
c = {'b': 2, 'd': 90}

Scal wszystko za pomocą jednej linii i zwróć obiekt dict za pomocą

c = dict(a.items() + b.items() + c.items())

Powracający

{'a': 9, 'b': 2, 'd': 90}
użytkownik6830669
źródło
6
Ponownie zadaj pytanie: nie jest to oczekiwany wynik. To powinna być ze swoimi wejściami: {'a': 9, 'b': 9, 'd': 90}. Brakuje wymogu „suma”.
Patrick Mevzek,