0

据我了解,由于count()为每个元素再次迭代列表,所以速度较慢。由于 Counter 和 dict 构造都遍历列表一次,所以 dict 构造和计数器时间结果不应该相似吗?

我使用https://stackoverflow.com/a/23909767/7125235作为获取时间值的代码参考。


import timeit

if __name__ == "__main__":
    code_block = """seen = {}
for i in l:
    if seen.get(i):
        seen[i] += 1
    else:
        seen[i] = 1
"""
    setup = """import random
import string
from collections import Counter
n=1000
l=[random.choice(string.ascii_letters) for x in range(n)]
"""

    t1 = timeit.Timer(
        stmt="Counter(l)",
        setup=setup,
    )

    t2 = timeit.Timer(
        stmt="[[x,l.count(x)] for x in set(l)]",
        setup=setup,
    )
    t3 = timeit.Timer(
        stmt=code_block,
        setup=setup,
    )

    print("Counter(): ", t1.repeat(repeat=3, number=10000))
    print("count():   ", t2.repeat(repeat=3, number=10000))
    print("seen{}:   ", t3.repeat(repeat=3, number=10000))

输出:

运行1:

Counter():  [0.32974308, 0.319977907, 0.301750341]
count():    [6.424047524000001, 6.417152854, 6.450776530000001]
seen{}:    [1.1089669810000018, 1.099655232, 1.116015376]
   

运行 2:

Counter():  [0.322483783, 0.32464020800000004, 0.33498838900000005]
count():    [6.3235339029999995, 6.48233445, 6.543396192000001]
seen{}:    [1.1192663550000006, 1.1072084830000009, 1.1155270229999985]
4

2 回答 2

1

TL;博士

Counter.__init__正在使用 C 循环(至少在 CPython 中)来计算可迭代的元素,请参阅https://github.com/python/cpython/blob/6b1ac809b9718a369aea67b99077cdd682be2238/Modules/_collectionsmodule.c#L2277


Counter是(主要,见下文)在 Python 中实现的,这意味着它的代码可以很容易地检查、调试甚至修改。

Counter.__init__在 CPython 3.8 中(不包括文档字符串):

def __init__(self, iterable=None, /, **kwds):
    super(Counter, self).__init__()
    self.update(iterable, **kwds)

Counter.update(不包括不相关的路径):

def update(self, iterable=None, /, **kwds):
    if iterable is not None:
        if isinstance(iterable, _collections_abc.Mapping):
            ...
        else:
            _count_elements(self, iterable)
    if kwds:
        ...

_count_elements

def _count_elements(mapping, iterable):
    mapping_get = mapping.get
    for elem in iterable:
        mapping[elem] = mapping_get(elem, 0) + 1

但是,在实现的正下方有一段非常重要的代码 + 注释_count_elements

try:  # Load C helper function if available
    from _collections import _count_elements
except ImportError:
    pass

换句话说,Counter使用 C 循环来计算元素,这本质上比您使用的 Python 循环要快。

我们可以做一个小实验。注释掉导入 C 函数的代码:

# try:   # Load C helper function if available
#     from _collections import _count_elements
# except ImportError:
#     pass

执行您的代码:

Counter():  [1.8369901, 1.8359803000000001, 1.940804]
seen{}:    [1.2392313000000001, 1.2483893999999998, 1.3157528000000003]

NowCounter实际上比普通的 dict 慢。

于 2021-01-10T18:52:03.680 回答
0

我更正了您的代码以对所有三个部分执行可比较的操作:导入相同的包(以平衡开销),然后搜索整个字符列表,而不是将其减少为唯一的字符串——这是您在“看到”代码:您只计算了每个字符中的一个。

import timeit

if __name__ == '__main__':
    code_block = '''seen = {}
for i in char:
    if seen.get(i):
        seen[i] += 1
    else:
        seen[i] = 1
'''

    common = 'import random;import string;from collections import Counter;n=1000;' + \
             'char=[random.choice(string.ascii_letters) for x in range(n)]'
    t1 = timeit.Timer(stmt='Counter(char)',
                      setup=common)

    t2 = timeit.Timer(stmt='[[x,char.count(x)] for x in set(char)]',
                      setup=common)
    t3 = timeit.Timer(stmt=code_block,
                      setup=common)

    print("Counter(): ", t1.repeat(repeat=3, number=10000))
    print("count():   ", t2.repeat(repeat=3, number=10000))
    print("seen{}:    ", t3.repeat(repeat=3, number=10000))

输出:

Counter():  [0.48620019999999997, 0.49807440000000014, 0.3896322000000001]
count():    [9.7432961, 9.620701299999999, 9.674791500000001]
seen{}:     [1.4734368999999994, 1.462895500000002, 1.4655799000000016]

Counter正如预期的那样,这似乎是最快的。

于 2021-01-10T18:49:46.913 回答