【问题标题】:Fastest way to generate a dict from list where key == value从 key == value 的列表生成字典的最快方法
【发布时间】:2018-10-04 15:35:54
【问题描述】:

我有一个清单,比如说:

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

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

my_dict = {item: item for item in my_list}

或:

my_dict = dict(zip(my_list, my_list))

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

例如,以下构造:

my_dict = {key: SOMETHING for key in keys}

翻译成更快:

my_dict = dict.fromkeys(k, SOMETHING)

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


编辑

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


编辑 2

dict.fromitems() 之类的方法比这个特定用例的应用范围更广,因为:

dict.fromitems(keys, values)

原则上可以同时替代两者:

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

和:

dict(zip(keys, values))

【问题讨论】:

  • 不,没有。
  • 我真的不明白为什么有人会首先需要这样的东西......你能给我们一个用例吗?也许我们可以找到一种更好的方法来做你想做的事
  • dict(zip 版本进行两个函数调用(尽管它们是 C 调用,比 Python 调用快)。但是zip 必须构建一堆元组,虽然这是一个便宜的操作,但 dict comp 避免了这种情况。
  • 时间问题是你代码的瓶颈吗?
  • 还有,弗雷德说的。你打算在任何阶段改变价值观吗?如果没有,为什么不直接使用一套?

标签: python list performance dictionary


【解决方案1】:

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

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

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

>>> 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(),到两个 单独 em> 键和值的迭代器)。

在 C 中向 dict 类添加一个单独的方法来处理这个问题是没有意义的,因为

  1. 无论如何都不是一个足够常见的用例(创建键和值相等的映射不是常见的需求)
  2. 在 C 中不会比使用字典理解时快得多无论如何

【讨论】:

  • 感谢您的见解。但是,我相信从两个可迭代对象创建 dict 的构造可能是一个足够大的用例来证明用 C 实现的 dict.fromitems(keys, values) 方法。这将比理解或使用 zip() 快得多,类似于list(items)[i for i in items] 快。我希望在这种特定情况下可能会有一些技巧。
  • @norok2: list() 可以更快,因为构建列表对象的操作非常简单,您甚至可以在已知items 的大小时预先设置新列表对象的大小。列表比较主要是较慢,因为它必须在添加项目时进行动态调整大小,因为您无法再获取大小提示。字典不能预先调整大小,所以dict.fromitems(keys, values)不会会更快。
  • @norok2:即使速度有小幅提升(zip() 确实必须为那里的每一对创建一个元组,所以你可以使用专用的dict.fromitems() 方法来避免这种情况),你必须说服 Python 核心开发人员,增加这种方法的额外成本是值得的。成本是:未来的维护、文档更新、使用混乱(dict(zip(...)) 已经提供了相同的功能)。我不认为他们会咬人,因为没有真正的好处,没有足够的用例需要这种程度的优化。
【解决方案2】:

这是禅宗的答案。字典理解循环本身很快,这不是瓶颈。正如 Martijn Pieters 所说,时间花在了:

  1. 访问密钥和;
  2. 计算密钥的__hash__
  3. 可能您的__hash__ 不好,并且您有很多冲突,导致字典插入很昂贵。

不用担心循环。如果构建字典需要很长时间,那是因为这些操作很慢。

【讨论】:

    【解决方案3】:

    使用答案here 的结果,我们创建一个继承defaultdict 的新类,并覆盖其missing 属性以允许将密钥传递给default_factory:

    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
    

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

    my_dict = keydefaultdict(lambda x: x)
    

    然后,当您需要对不映射到自身的键进行映射时,您只需更新 那些 值。

    时间安排。

    子类化defaultdict:

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

    结果:

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

    听写理解

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

    结果:

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

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

    仅访问原始值的一部分

    子类化defaultdict:

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

    结果:

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

    听写理解

    %%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
    

    结果:

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

    【讨论】:

    • 哇,这深入到了内部!当您说subclass the default dict 时,您的答案中my_dict 的类型是什么?它是一个普通的字典吗?您能否timeit 并针对所提供的示例给出基准?您可以添加如何将其传递给列表吗?
    • @devssh my_dict 将是以下所有的实例:dictdefaultdictkeydefaultdict
    • 您能否更具体地说明如何将它用于给定的list?另外,能否请您提供一些时间安排?
    • 这肯定不会更快,因为您为每个值添加了一个函数调用,而不是让值预先可用。 Python 函数调用在这里相对昂贵。
    • 你甚至不应该使用 lambda。只需 子类 dict 并使其 __missing__ 只需将值设置为键。
    猜你喜欢
    • 1970-01-01
    • 2015-05-15
    • 2014-03-28
    • 2015-07-24
    • 2023-03-05
    • 1970-01-01
    • 2019-03-12
    • 2021-01-23
    • 1970-01-01
    相关资源
    最近更新 更多