Python 中对 Counter 对象求和的有效方法
Efficient way to sum Counter objects in Python
是否有更有效的方法或库可以更快地添加 Counter 对象?
到目前为止,我正在使用以下代码,我需要比它更快的代码:
cnt = sum([Counter(objects) for objects in object_list], Counter())
我无法弄清楚 sum
函数如何处理您发布的代码。使用更简单函数的快速测试似乎显示了该代码块的改进:
from collections import Counter
import random
import timeit
def func1(objs):
count = Counter()
for obj in objs:
count.update(obj)
return count
def func2(objs):
return sum([Counter(obj) for obj in objs], Counter())
length = 100
objs = [[random.randint(0, 100) for i in range(length)] for i in range(length)]
time1 = timeit.timeit(lambda: func1(objs), number=100)
time2 = timeit.timeit(lambda: func2(objs), number=100)
print(f"Proposed Solution (t1): {time1}")
print(f"Question Solution (t2): {time2}")
print(f"t1 < t2: {time1 < time2}")
print(f"f1 == f2 {func1(objs) == func2(objs)}")
Proposed Solution (t1): 0.047416953
Question Solution (t2): 0.433098309
t1 < t2: True
f1 == f2 True
不要制作大量的临时 Counter
s,只制作一个,然后让它计数 一切:
from collections import Counter
from itertools import chain
cnt = Counter(chain.from_iterable(object_list)
从较小的输入中生成一堆单独的 Counter
s 是昂贵的,并且剥夺了 Counter
用于计算输入迭代的 C 加速器为您提供的一些性能优势。使用 sum
将它们组合起来使其成为 Schlemiel the Painter's algorithm,因为它会产生大量逐渐增加大小的临时 Counter
(工作最终大致为 O(m * n)
,其中 n
是计数的项目总数,m
是它们被拆分的对象数)。在扁平化的输入迭代器上计数一次可以将工作减少到 O(n)
.
将可迭代对象的可迭代对象展平为单个输入流并将其全部计数一次显着 减少了运行时间,尤其是对于大量较小的对象。
像这样使用 chain.from_iterable
等同于:
cnt = Counter(item for object in object_list for item in object)
但是在CPython参考解释器上将工作推到了C层;如果 object_list
的内容也是用 C 实现的所有内置类型,那么当您使用 chain.from_iterable
时根本不会执行任何字节码,删除 lot解释器开销。
如果您必须有一堆 Counter
,至少可以通过就地更新累加器 Counter
来避免 Schlemiel the Painter 的算法。你可以用一种丑陋的方式把它单行化(这仍然是临时的 Counter
s,但至少它不会逐渐地 变大 每次都扔掉的临时文件)有:
cnt = functools.reduce(operator.iadd, map(Counter, object_list), Counter())
或使其更具可读性(并避免任何额外的临时文件):
cnt = Counter()
for obj in object_list:
cnt.update(obj) # cnt += Counter(obj) works, but involves unnecessary temporary
是否有更有效的方法或库可以更快地添加 Counter 对象?
到目前为止,我正在使用以下代码,我需要比它更快的代码:
cnt = sum([Counter(objects) for objects in object_list], Counter())
我无法弄清楚 sum
函数如何处理您发布的代码。使用更简单函数的快速测试似乎显示了该代码块的改进:
from collections import Counter
import random
import timeit
def func1(objs):
count = Counter()
for obj in objs:
count.update(obj)
return count
def func2(objs):
return sum([Counter(obj) for obj in objs], Counter())
length = 100
objs = [[random.randint(0, 100) for i in range(length)] for i in range(length)]
time1 = timeit.timeit(lambda: func1(objs), number=100)
time2 = timeit.timeit(lambda: func2(objs), number=100)
print(f"Proposed Solution (t1): {time1}")
print(f"Question Solution (t2): {time2}")
print(f"t1 < t2: {time1 < time2}")
print(f"f1 == f2 {func1(objs) == func2(objs)}")
Proposed Solution (t1): 0.047416953
Question Solution (t2): 0.433098309
t1 < t2: True
f1 == f2 True
不要制作大量的临时 Counter
s,只制作一个,然后让它计数 一切:
from collections import Counter
from itertools import chain
cnt = Counter(chain.from_iterable(object_list)
从较小的输入中生成一堆单独的 Counter
s 是昂贵的,并且剥夺了 Counter
用于计算输入迭代的 C 加速器为您提供的一些性能优势。使用 sum
将它们组合起来使其成为 Schlemiel the Painter's algorithm,因为它会产生大量逐渐增加大小的临时 Counter
(工作最终大致为 O(m * n)
,其中 n
是计数的项目总数,m
是它们被拆分的对象数)。在扁平化的输入迭代器上计数一次可以将工作减少到 O(n)
.
将可迭代对象的可迭代对象展平为单个输入流并将其全部计数一次显着 减少了运行时间,尤其是对于大量较小的对象。
像这样使用 chain.from_iterable
等同于:
cnt = Counter(item for object in object_list for item in object)
但是在CPython参考解释器上将工作推到了C层;如果 object_list
的内容也是用 C 实现的所有内置类型,那么当您使用 chain.from_iterable
时根本不会执行任何字节码,删除 lot解释器开销。
如果您必须有一堆 Counter
,至少可以通过就地更新累加器 Counter
来避免 Schlemiel the Painter 的算法。你可以用一种丑陋的方式把它单行化(这仍然是临时的 Counter
s,但至少它不会逐渐地 变大 每次都扔掉的临时文件)有:
cnt = functools.reduce(operator.iadd, map(Counter, object_list), Counter())
或使其更具可读性(并避免任何额外的临时文件):
cnt = Counter()
for obj in object_list:
cnt.update(obj) # cnt += Counter(obj) works, but involves unnecessary temporary