Собрать взвешенные значения в 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
Результат очевиден
Ничего соизмеримого по скорости с вашим вариантом скорее всего не найдется.
Операции, требуемые для выполнения 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))
В лучшем случае оба варианта медленнее вашего, в худшем - прямо вот сильно медленнее.
На самом деле получается, что основной функционал 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})
Замеры предложенных ответов с линейной асимптотикой: 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