关于python:从key == value的列表生成dict的最快方法

Fastest way to generate a dict from list where key == value

我有一个清单,说:

1
2
NUM = 100
my_list = list(range(NUM))

我想生成一个dict,其中键等于值,如:

1
my_dict = {item: item for item in my_list}

要么:

1
my_dict = dict(zip(my_list, my_list))

我已经运行了一些微基准测试,看起来他们有相似的速度,但我希望第二个更快,因为循环应该在C中发生。

例如,以下构造:

1
my_dict = {key: SOMETHING for key in keys}

转化为更快:

1
my_dict = dict.fromkeys(k, SOMETHING)

所以,我的问题是:{x: x for x in my_list}是否有类似的这种结构?

编辑

我检查了dir(dict),似乎没有任何方向(我希望它被称为像dict.fromitems())。

编辑2

dict.fromitems()这样的方法比这个特定的用例有更广泛的应用,因为:

1
dict.fromitems(keys, values)

原则上可以替代两者:

1
{k, v for k, v in zip(keys, values)}

和:

1
dict(zip(keys, values))


不,没有更快的方法可供字典使用。

这是因为性能成本全部来自迭代器处理每个项目,计算其哈希值并将密钥插入字典数据哈希表结构(包括动态增长这些结构)。相比之下,执行字典理解字节码实际上是微不足道的。

dict(zip(it, it)){k: k for k in it}dict.fromkeys(it)的速度都接近:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
>>> from timeit import Timer
>>> tests = {
...     'dictcomp': '{k: k for k in it}',
...     'dictzip': 'dict(zip(it, it))',
...     'fromkeys': 'dict.fromkeys(it)',
... }
>>> timings = {n: [] for n in tests}
>>> for magnitude in range(2, 8):
...     it = range(10 ** magnitude)
...     for name, test in tests.items():
...         peritemtimes = []
...         for repetition in range(3):
...             count, total = Timer(test, 'from __main__ import it').autorange()
...             peritemtimes.append(total / count / (10 ** magnitude))
...         timings[name].append(min(peritemtimes))  # best of 3
...
>>> for name, times in timings.items():
...     print(f'{name:>8}', *(f'{t * 10 ** 9:5.1f} ns' for t in times), sep=' | ')
...
dictcomp |  46.5 ns |  47.5 ns |  50.0 ns |  79.0 ns | 101.1 ns | 111.7 ns
 dictzip |  49.3 ns |  56.3 ns |  71.6 ns | 109.7 ns | 132.9 ns | 145.8 ns
fromkeys |  33.9 ns |  37.2 ns |  37.4 ns |  62.7 ns |  87.6 ns |  95.7 ns

这是每种技术的每件物品成本表,从100到1000万件。随着增加哈希表结构的额外成本累积,时间会上升。

当然,dict.fromkeys()可以更快地处理项目,但它不比其他进程快一个数量级。它的(小)速度优势不是来自能在这里迭代C;差别在于完全不必每次迭代都更新值指针;所有键都指向单值引用。

zip()比较慢,因为它构建了额外的对象(为每个键值对创建一个2项元组不是一个免费的操作),并且它增加了进程中涉及的迭代器数量,你来自单个迭代器对于字典理解和dict.fromkeys(),到3个迭代器(dict()迭代委托,通过zip(),到键和值的两个独立的迭代器)。

dict类添加单独的方法以在C中处理它是没有意义的,因为

  • 反正不是一个常见的用例(创建一个带键和值相等的映射不是常见的需求)
  • 无论如何,C语言的速度都不会比字典理解速度快得多。

  • 使用这里的答案结果,我们创建了一个子类defaultdict的新类,并覆盖其缺少的属性以允许将键传递给default_factory:

    1
    2
    3
    4
    5
    6
    7
    8
    from collections import defaultdict
    class keydefaultdict(defaultdict):
        def __missing__(self, key):
            if self.default_factory is None:
                raise KeyError(key)
            else:
                ret = self[key] = self.default_factory(key)
                return ret

    现在,您可以通过执行以下操作来创建您要查找的字典类型:

    1
    my_dict = keydefaultdict(lambda x: x)

    然后,只要您需要为不映射到自己的键进行映射,您只需更新这些值。

    计时。

    子类defaultdict

    1
    2
    3
    %%timeit
    my_dict = keydefaultdict(lambda x: x)
    for num in some_numbers: my_dict[num] == num

    结果:

    1
    4.46 s ± 71.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

    Dict理解

    1
    2
    3
    %%timeit
    my_dict = {x: x for x in some_numbers}
    for num in some_numbers: my_dict[num] == num

    结果:

    1
    1.19 s ± 20.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

    当您需要访问大约17%的原始值时,这两者变得可比较。如果您需要更少,那就更好了:

    仅访问原始值的一部分

    子类defaultdict

    1
    2
    3
    4
    %%timeit
    frac = 0.17
    my_dict = keydefaultdict(lambda x: x)
    for num in some_numbers[:int(len(some_numbers)*frac)]: my_dict[num] == num

    结果:

    1
    770 ms ± 4.69 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

    Dict理解

    1
    2
    3
    4
    %%timeit
    frac = 0.175
    my_dict = {x: x for x in some_numbers}
    for num in some_numbers[:int(len(some_numbers)*frac)]: my_dict[num] == num

    结果:

    1
    781 ms ± 4.03 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)