【问题标题】:is there a faster way to get multiple keys from dictionary?有没有更快的方法从字典中获取多个键?
【发布时间】:2018-09-08 09:28:01
【问题描述】:

我有一本字典:

d = {'a':1, 'b':2, 'c':3, 'd':4}

然后我有一个键列表:

l = ['a', 'b', 'z']

我想要的结果是:

[1, 2, None]

到目前为止,我正在做的是:

[d.get(k) for k in l]

有没有更快的方法?也许没有for

【问题讨论】:

  • list(map(d.get, l)) 可能会稍微快一些。但在这个级别的微优化中,我建议是否将其转移到 C 或其他较低级别的语言上。
  • [d.get(k) for k in l] 是一种完美的方式。你到底对什么不满意?表现?可读性?简洁?
  • 很遗憾operator.itemgetter 总是提出KeyError 而不是允许未定义键的默认值;否则你可以使用itemgetter(*l)(d)

标签: python performance dictionary key-value


【解决方案1】:

你可以使用:

>>> list(map(d.get, l))
[1, 2, None]

它有两个优点:

  • 它只执行一次 d.get 查找 - 不是每次迭代
  • 仅限 CPython:因为dict.get 是用 C 实现的,map 是用 C 实现的,所以可以避免函数调用中的 Python 层(粗略地说细节有点复杂)。

至于计时(在 Jupyter 笔记本中的 Python 3.6 上执行):

d = {'a':1, 'b':2, 'c':3, 'd':4}
l = ['a', 'b', 'z']

%timeit list(map(d.get, l))
594 ns ± 41.7 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

%timeit [d.get(k) for k in l]
508 ns ± 17.1 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

请注意,在这种情况下这实际上更慢!这是因为对于短迭代,maplist 开销占主导地位。因此,如果您希望在短迭代上更快地使用您的方法。

随着l 的延长,您会看到list(map(...)) 最终会变得更快:

d = {'a':1, 'b':2, 'c':3, 'd':4}
l = [random.choice(string.ascii_lowercase) for _ in range(10000)]

%timeit list(map(d.get, l))
663 µs ± 64.6 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

%timeit [d.get(k) for k in l]
1.13 ms ± 7.55 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

但是,这仍然“只是”快了 2 倍。

【讨论】:

    【解决方案2】:

    需要注意的是,这里的瓶颈不是字典的大小,而是键列表的大小(当然还有方法查找时间,散列正在查找的键所需的时间,等等)

    考虑以下几点:

    from timeit import Timer
    
    d = {1: 'a'}
    keys = [1 for _ in range(1000)]
    
    def _map():
        return list(map(d.get, keys))
    
    def _map_single_lookup():
        g = d.get
        return list(map(g, keys))
    
    def _list_comp():
        return [d.get(key) for key in keys]
    
    def _list_comp_single_lookup():
        g = d.get
        return [g(key) for key in keys]
    
    print(min(Timer(_map).repeat(100, 100)))
    print(min(Timer(_map_single_lookup).repeat(100, 100)))
    print(min(Timer(_list_comp).repeat(100, 100)))
    print(min(Timer(_list_comp_single_lookup).repeat(100, 100)))
    
    #  0.009307396466818774
    #  0.009261678214412816
    #  0.018456645101335933
    #  0.011634828724497837
    

    【讨论】:

    • 您的_map_map_single_lookup 完全等价。它们都只需要一次查找d.get
    • @jmd_dk 我不能 100% 说实话。 _map_single_lookup 始终更快
    • 但不是因为d.get 查找时间。正如list* 测试所证明的那样,这种差异应该更大。
    • 如果为真,那很有趣。但从语法上讲,这两个查找d.get 的次数不同是不可能的,因为唯一的迭代行为发生在函数内部(这里是map)。也就是说,每个子表达式都被计算一次,而不是列表推导式。
    • @jmd_dk 我同意。那一定只是侥幸。
    【解决方案3】:

    更快的替代方法是将itemgetterdefaultdict 一起使用(因为itemgetter 不支持dict.get 这样的默认值,以防密钥不存在)

    from collections import defaultdict
    from timeit import Timer
    from operator import itemgetter
    
    d = defaultdict(lambda: None)
    d[1] = 'a'
    keys = [1 for _ in range(1000)]
    
    def _map():
        return list(map(d.get, keys))
    
    def _getter():
        return list(itemgetter(*keys)(d))
    
    print(min(Timer(_map).repeat(100, 100)))
    print(min(Timer(_getter).repeat(100, 100)))
    
    # 0.0074976040767260055
    # 0.0021861597102568187
    

    编辑为不存在的键(整数和字符串)添加了计时。对性能没有显着影响。

    from collections import defaultdict
    from timeit import Timer
    from operator import itemgetter
    
    d = defaultdict(lambda: None)
    d[1] = 'a'
    non_existing_keys_int = [2 for _ in range(1000)]
    non_existing_keys_string = ['a' for _ in range(1000)]
    
    def get_non_existing_keys_int():
        return list(itemgetter(*non_existing_keys_int)(d))
    
    def get_non_existing_keys_string():
        return list(itemgetter(*non_existing_keys_string)(d))
    
    print(min(Timer(get_non_existing_keys_int).repeat(100, 100)))
    print(min(Timer(get_non_existing_keys_string).repeat(100, 100)))
    
    #  0.002647169132724954
    #  0.002408539023506795
    

    【讨论】:

    • 您是否有任何计时包括不存在的键和非整数?这些可能会对性能产生重大影响。
    • @MSeifert 我更新了答案。不存在的键不会对性能产生重大影响
    猜你喜欢
    • 2011-10-05
    • 1970-01-01
    • 2016-12-18
    • 2020-01-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-11-18
    • 1970-01-01
    相关资源
    最近更新 更多