มีวิธี pythonic เพื่อรวมสอง dicts (เพิ่มค่าสำหรับคีย์ที่ปรากฏในทั้งสอง)?


477

ตัวอย่างเช่นฉันมีสอง dicts:

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

ฉันต้องการวิธี pythonic ของ 'การรวม' สอง dicts ที่ผลคือ:

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

กล่าวคือหากคีย์ปรากฏในทั้งสอง dicts ให้เพิ่มค่าของมันหากปรากฏในเพียงหนึ่ง dict ให้เก็บค่าไว้

คำตอบ:


835

การใช้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})

ตัวนับนั้นเป็นคลาสย่อยของdictดังนั้นคุณยังสามารถทำทุกอย่างกับพวกเขาตามปกติที่คุณทำกับประเภทนั้นเช่นซ้ำผ่านคีย์และค่าของพวกเขา


4
มีหลายเคาน์เตอร์ที่จะรวมในลักษณะนี้ sum(counters)ไม่ทำงานโชคไม่ดี
ดร. Jan-Philip Gehrcke

27
@ ม.ค. PhilipGehrcke: ให้เป็นค่าเริ่มต้นด้วยsum() sum(counters, Counter())
Martijn Pieters

5
ขอบคุณ อย่างไรก็ตามวิธีนี้ได้รับผลกระทบจากการสร้างวัตถุระดับกลางเนื่องจากข้อสรุปรวมนั้นใช่ไหม?
ดร. Jan-Philip Gehrcke

6
@ Jan-PhilipGehrcke: ตัวเลือกอื่นของคุณคือการใช้การวนซ้ำและ+=การรวมเข้าด้วยกัน แล้วres = counters[0] for c in counters[1:]: res += c
Martijn Pieters

3
ฉันชอบวิธีการนี้! หากมีคนชอบเก็บสิ่งต่าง ๆ ไว้ใกล้เคียงกับการประมวลผลพจนานุกรมหนึ่งสามารถใช้update()แทน+=: for c in counters[1:]: res.update(c).
ดร. Jan-Philip Gehrcke

119

โซลูชันทั่วไปที่ใช้กับค่าที่ไม่ใช่ตัวเลขได้ดี:

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)])

หรือทั่วไปมากขึ้น:

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)])

ตัวอย่างเช่น:

>>> 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}

27
คุณสามารถใช้for k in b.viewkeys() & a.viewkeys()เมื่อใช้ python 2.7และข้ามการสร้างชุด
Martijn Pieters

เหตุใดจึงset(a)ส่งคืนชุดของคีย์แทนที่จะเป็นชุดของ tuples เหตุผลนี้คืออะไร
Sarsaparilla

1
@HaiPhan: เพราะจะทำซ้ำปุ่มมากกว่าไม่เกินคู่ kv cf list({..}), for k in {...}ฯลฯ
georg

2
@ Craicerjack: ใช่ฉันใช้operator.mulเพื่อให้ชัดเจนว่ารหัสนี้เป็นรหัสทั่วไปและไม่ จำกัด จำนวน
georg

6
คุณสามารถเพิ่มตัวเลือกที่รองรับ Python 3 ได้หรือไม่? {**a, **b, **{k: op(a[k], b[k]) for k in a.keys() & b}}ควรทำงานใน 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}

1
จะไม่ใช้for x in set(itertools.chain(A, B))ตรรกะมากขึ้นหรือไม่ เช่นเดียวกับการใช้ set on dict นั้นเป็นเรื่องไร้สาระเพราะคีย์นั้นมีความพิเศษอยู่แล้ว? ฉันรู้ว่ามันเป็นเพียงวิธีอื่นที่จะได้รับชุดของปุ่ม แต่ฉันคิดว่ามันมากขึ้นกว่าการใช้สับสนitertools.chain(หมายความว่าคุณรู้ว่าitertools.chainไม่)
jeromej

45

บทนำ: มีวิธีแก้ไขปัญหาที่ดีที่สุด (อาจ) แต่คุณต้องรู้และจดจำมันและบางครั้งคุณต้องหวังว่าเวอร์ชั่น Python ของคุณจะไม่แก่เกินไปหรือมีปัญหาอะไรเกิดขึ้น

จากนั้นก็มีทางออก 'แฮ็ค' มากที่สุด มันยอดเยี่ยมและสั้น แต่บางครั้งก็ยากที่จะเข้าใจอ่านและจดจำ

แม้ว่าจะมีทางเลือกอื่นที่จะพยายามบูรณาการล้อ - ทำไมต้องคิดค้นล้อใหม่ - โดยทั่วไปเพราะเป็นวิธีที่ดีมากในการเรียนรู้ (และบางครั้งก็เป็นเพราะเครื่องมือที่มีอยู่แล้วไม่ได้ทำสิ่งที่คุณต้องการและ / หรือวิธีที่คุณต้องการ) และวิธีที่ง่ายที่สุดถ้าคุณไม่รู้หรือ อย่าลืมเครื่องมือที่สมบูรณ์แบบสำหรับปัญหาของคุณ

ดังนั้นฉันเสนอที่จะบูรณาการล้อของCounterชั้นเรียนจากcollectionsโมดูล (อย่างน้อยบางส่วน):

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}

อาจมีวิธีอื่นในการใช้งานและมีเครื่องมือในการทำเช่นนั้นอยู่แล้ว แต่ก็เป็นเรื่องดีที่จะเห็นภาพว่าสิ่งต่าง ๆ ใช้งานได้อย่างไร


3
ดีสำหรับพวกเราที่ยังอยู่ที่ 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)

13

อันที่ไม่มีการนำเข้าพิเศษ!

เป็นมาตรฐานแบบ pythonic ที่เรียกว่าEAFP (ง่ายกว่าที่จะขอการให้อภัยมากกว่าการอนุญาต) โค้ดด้านล่างนี้ใช้มาตรฐานไพธ อน

# 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

แก้ไข: ขอบคุณjerzykสำหรับคำแนะนำการปรับปรุงของเขา


5
อัลกอริทึม n ^ 2 จะช้ากว่าวิธีการเคาน์เตอร์อย่างมาก
Joop

@DeveshSaini ดีกว่า แต่ยังดีที่สุดย่อย :) เช่น: คุณต้องการเรียงลำดับจริง ๆ หรือไม่? แล้วทำไมสองลูป คุณมีคีย์ทั้งหมดใน newdict อยู่แล้วเพียงคำใบ้เล็ก ๆ เพื่อเพิ่มประสิทธิภาพ
Jerzyk

มีการวางอัลกอริทึม n ^ 1 แทนอัลกอริทึม n ^ 2 ก่อนหน้านี้ @Joop
Devesh Saini

11

แน่นอนข้อสรุปCounter()คือวิธีที่ pythonic ที่สุดที่จะไปในกรณีดังกล่าว แต่เพียงว่ามันจะส่งผลให้ค่าบวก นี่คือตัวอย่างและอย่างที่คุณเห็นว่าไม่มีcผลลัพธ์หลังจากลบcค่าในBพจนานุกรม

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})

นั่นเป็นเพราะCounters ได้รับการออกแบบมาเพื่อทำงานกับจำนวนเต็มบวกเพื่อแสดงจำนวนการรัน (จำนวนลบนั้นไม่มีความหมาย) แต่เพื่อช่วยในกรณีการใช้งานเหล่านั้นงูหลามจัดทำเอกสารข้อ จำกัด ขั้นต่ำและประเภทดังนี้:

  • คลาสตัวนับเป็นคลาสย่อยของพจนานุกรมโดยไม่มีข้อ จำกัด เกี่ยวกับคีย์และค่า ค่ามีวัตถุประสงค์เพื่อเป็นตัวเลขที่แสดงถึงการนับ แต่คุณสามารถเก็บอะไรก็ได้ในฟิลด์ค่า
  • most_common()วิธีการต้องมีเพียงว่าค่าที่จะสามารถสั่งซื้อได้
  • สำหรับการดำเนินการในสถานที่เช่นc[key] += 1ประเภทของค่าต้องรองรับการเพิ่มและการลบเท่านั้น ดังนั้นจึงสามารถใช้เศษส่วนลอยและทศนิยมและสนับสนุนค่าลบได้ เช่นเดียวกับupdate()และsubtract()อนุญาตให้มีค่าลบและศูนย์สำหรับทั้งอินพุตและเอาต์พุต
  • วิธีการหลายชุดได้รับการออกแบบสำหรับกรณีการใช้งานที่มีค่าบวกเท่านั้น อินพุตอาจเป็นค่าลบหรือศูนย์ แต่จะสร้างเฉพาะเอาต์พุตที่มีค่าเป็นบวก ไม่มีข้อ จำกัด ประเภท แต่ประเภทค่าจำเป็นต้องรองรับการบวกการลบและการเปรียบเทียบ
  • elements()วิธีการต้องนับจำนวนเต็ม มันจะละเว้นจำนวนศูนย์และจำนวนลบ

ดังนั้นสำหรับการแก้ไขปัญหานั้นหลังจากสรุป Counter ของคุณคุณสามารถใช้Counter.updateเพื่อให้ได้ผลลัพธ์ตามที่ต้องการ มันทำงานเหมือนdict.update()แต่เพิ่มจำนวนแทนการแทนที่

In [24]: A.update(B)

In [25]: A
Out[25]: Counter({'d': 5, 'b': 5, 'a': 1, 'c': -1})

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}

หรือ

ทางเลือกคุณสามารถใช้ตัวนับเป็น @Martijn ได้กล่าวถึงข้างต้น


7

สำหรับทั่วไปมากขึ้นและขยายวิธีการตรวจสอบmergedict มันใช้singledispatchและสามารถผสานค่าตามชนิดของมัน

ตัวอย่าง:

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'}

5

จาก python 3.5: การรวมและการรวม

ขอบคุณ @tokeinizer_fsj ที่บอกฉันในความคิดเห็นว่าฉันไม่ได้รับความหมายอย่างสมบูรณ์ของคำถาม (ฉันคิดว่าการเพิ่มหมายถึงเพียงแค่เพิ่มกุญแจที่ในที่สุดซึ่งแตกต่างในสองพจนานุกรมและแทนฉันหมายถึงค่าคีย์ทั่วไป ควรสรุปรวม) ดังนั้นฉันจึงเพิ่มลูปนั้นก่อนการรวมเพื่อให้พจนานุกรมที่สองมีผลรวมของคีย์ทั่วไป พจนานุกรมสุดท้ายจะเป็นพจนานุกรมที่มีค่าจะคงอยู่ในพจนานุกรมใหม่ที่เป็นผลมาจากการรวมกันของทั้งสองดังนั้นฉันจึงแก้ไขปัญหาได้ การแก้ปัญหาที่ถูกต้องจากงูหลาม 3.5 และรุ่นต่อไปนี้

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}

รหัสนำมาใช้ใหม่

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))

วิธีการผสานพจนานุกรมนี้จะไม่เพิ่มค่าสำหรับคีย์ทั่วไป ในคำถามที่ค่าที่ต้องการสำหรับคีย์bเป็น5(2 + 3) 3แต่วิธีการของคุณจะกลับมา
tokenizer_fsj

4

นอกจากนี้โปรดทราบว่าa.update( b )เร็วกว่า 2xa + 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

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)

คุณสามารถพูดคุยเรื่องนี้ได้อย่างง่ายดาย:

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)

จากนั้นสามารถใช้จำนวน dicts ใดก็ได้


2

นี่เป็นทางออกที่ง่ายสำหรับการรวมสองพจนานุกรมที่+=สามารถนำไปใช้กับค่าได้โดยจะต้องวนซ้ำในพจนานุกรมเพียงครั้งเดียว

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}

1

โซลูชันนี้ใช้งานง่ายใช้เป็นพจนานุกรมปกติ แต่คุณสามารถใช้ฟังก์ชันผลรวมได้

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}

1

เกี่ยวกับ:

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 ) )

เอาท์พุท:

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

0

การแก้ปัญหาข้างต้นนั้นยอดเยี่ยมสำหรับสถานการณ์ที่คุณมีจำนวนCounterน้อย หากคุณมีรายการใหญ่ของพวกเขาบางสิ่งเช่นนี้ดีกว่ามาก:

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})

วิธีการแก้ปัญหาดังกล่าวเป็นข้อสรุปCounterโดย:

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

สิ่งนี้ทำสิ่งเดียวกัน แต่ฉันคิดว่ามันจะช่วยให้เห็นว่ามันทำอะไรได้อย่างมีประสิทธิภาพภายใต้


0

การรวมสาม dicts a, b, c ในบรรทัดเดียวโดยไม่มีโมดูลหรือ libs อื่น ๆ

ถ้าเรามีสาม dicts

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

ผสานทั้งหมดด้วยบรรทัดเดียวและส่งคืนวัตถุ dict ที่ใช้

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

การคืน

{'a': 9, 'b': 2, 'd': 90}

6
อ่านคำถามนี้ไม่ใช่ผลลัพธ์ที่คาดหวัง มันควรจะเป็นกับอินพุตของคุณ: {'a': 9, 'b': 9, 'd': 90}. คุณไม่มีข้อกำหนด "รวม"
Patrick Mevzek
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.