Собрать взвешенные значения в Counter

Есть список пар (ключ,значение), причём ключи могут повторяться. Я хочу собрать всё в Counter так, чтобы значения по одинаковым ключам просуммировались.

Могу это сделать так: tio.run

from collections import Counter

a = [("A",3), ("B",10), ("A",8), ("C",1)]

c = Counter()
for k,v in a: c[k] += v

print(c) # Counter({'A': 11, 'B': 10, 'C': 1})

Есть ли более красивый способ получить то же самое?


Ответы (4 шт):

Автор решения: Виктор

"Есть ли более красивый способ получить то же самое?" - красота у каждого своя:)

То же самое, без дополнительных модулей:

a = [("A",3), ("B",10), ("A",8), ("C",1)]

c = {}
for k,v in a:
    c[k] = v if k not in c else c[k] + v

print(c)

Вывод: {'A': 11, 'B': 10, 'C': 1}

У подходов есть разная цель: красота или скорость

import timeit
from collections import Counter

def proc():
    a = [("A",3), ("B",10), ("A",8), ("C",1)]
    c = Counter()
    for k,v in a: c[k] += v

def proc2():
    a = [("A",3), ("B",10), ("A",8), ("C",1)]
    c = {}
    for k,v in a:
        c[k] = v if k not in c else c[k] + v

print(timeit.timeit(proc2, number=100000))
print(timeit.timeit(proc, number=100000))

Итог:

100000 прогонов
proc2: 0.0937448000004224
proc:  0.3667998000000807

1000000 прогонов
proc2: 0.9124658000000636                                                                                  
proc:  3.624726500000179

Результат очевиден

→ Ссылка
Автор решения: extrn

Ничего соизмеримого по скорости с вашим вариантом скорее всего не найдется.

Операции, требуемые для выполнения c[k] += v в Counter (прямом потомке dict) не переопределены, и работают примерно так же быстро как в dict, потому, что реализованы нативно. А вот все специфичные для Counter операции (кроме инициализации/апдейта с помощью iterable) написаны на python, и ожидаемо значительно более медленные.

Но вот пара вариантов для повышения эстетики

c = sum((Counter({k: v}) for k, v in a), start=Counter()) # В худшем случае O(n*n)
c = Counter(k for k, v in a for _ in range(v)) # В худшем случае O(n*max(v))

В лучшем случае оба варианта медленнее вашего, в худшем - прямо вот сильно медленнее.

→ Ссылка
Автор решения: CrazyElf

На самом деле получается, что основной функционал Counter-а вы вообще не используете, тогда уж лучше взять defaultdict, он будет быстрее (но всё-равно медленнее, чем "ручная" проверка словаря-счётчика на существование элементов):

from collections import defaultdict

a = [("A",3), ("B",10), ("A",8), ("C",1)]

c = defaultdict(int)
for k, v in a: 
    c[k] += v

print(c) # defaultdict(<class 'int'>, {'A': 11, 'B': 10, 'C': 1})
→ Ссылка
Автор решения: Qwertiy

Замеры предложенных ответов с линейной асимптотикой: Counter самый медленный - примерно в 2 раза хуже остальных, а со словарями все способы дают результат в пределах погрешности. tio.run

from collections import Counter, defaultdict
from random import randint
from timeit import timeit

def viaCounter():
  c = Counter()
  for k,v in a: c[k] += v
  return c

def viaDictCheck():
  c = {}
  for k,v in a: c[k] = v if k not in c else c[k] + v
  return c

def viaDictGet():
  c = {}
  for k,v in a: c[k] = c.get(k, 0) + v
  return c

def viaDefaultDict():
  c = defaultdict(int)
  for k,v in a: c[k] += v
  return c

ways = [viaCounter, viaDictCheck, viaDictGet, viaDefaultDict]

a = [("A",3), ("B",10), ("A",8), ("C",1)]
for f in ways: print(f"{f.__name__:16}", f())

for n in [10, 100, 1000, 10000]:
  print()
  print("===", n, "===")
  a = [(randint(0,1024), randint(0,1024)) for _ in range(n)]
  for f in ways: print(f"{f.__name__:16}", timeit(f, number=1000))

Результат:

viaCounter       Counter({'A': 11, 'B': 10, 'C': 1})
viaDictCheck     {'A': 11, 'B': 10, 'C': 1}
viaDictGet       {'A': 11, 'B': 10, 'C': 1}
viaDefaultDict   defaultdict(<class 'int'>, {'A': 11, 'B': 10, 'C': 1})

=== 10 ===
viaCounter       0.005354834022000432
viaDictCheck     0.000977317977230996
viaDictGet       0.001639115042053163
viaDefaultDict   0.00273122702492401

=== 100 ===
viaCounter       0.041808484005741775
viaDictCheck     0.011960842995904386
viaDictGet       0.01808635302586481
viaDefaultDict   0.02747832500608638

=== 1000 ===
viaCounter       0.40349273895844817
viaDictCheck     0.16457551100756973
viaDictGet       0.239735366019886
viaDefaultDict   0.24876780400518328

=== 10000 ===
viaCounter       3.3196506010135636
viaDictCheck     1.8371806619688869
viaDictGet       1.956588956003543
viaDefaultDict   1.7752377989818342
→ Ссылка