为什么在字典中使用 dictionary.keys() 比直接使用 key in dictionary 更慢?

5
阅读Michael Driscoll的《Python 101》时,我看到他对检查字典中键是否存在的解释。我在自己的机器上使用了一个包含键“a”到“z”的字典,其中值是它们的顺序,并使用一个测量检索键“t”(随机选择)时间的函数来检查它。
这是我的函数:
def f(d, flag):
start_time = time.time()
if flag:
    print("t" in d)
else:
    print("t" in d.keys())
print("--- %s seconds ---" % (time.time() - start_time))

以下是结果:

>>> f(dict,True)
True
--- 0.03937530517578125 seconds ---

>>> f(dict,False)
True
--- 0.05114388465881348 seconds ---

但是,我还是不理解。我认为key in dict.keys()会在迭代更小的集合上进行,这样会更快。是不是inkeys()的实现有什么特殊之处导致了这种情况?


2
因为您需要先创建一个额外的对象。 - Martijn Pieters
但是为什么被踩了呢? - CIsForCookies
1
你为什么认为 dict.keys() 会测试一个更小的集合?那个集合不包括 key in dict 会包括什么? - Martijn Pieters
我认为字典是一组键值对的集合,我会搜索每个键值对而不是搜索一个只有一个项目对象的列表,并且访问键值对也会消耗CPU时间。此外,我认为返回键将是一个小而快速的操作。 - CIsForCookies
请注意,如果你正在使用 Python 2,则时间会有很大不同 https://stackoverflow.com/questions/24540975/why-does-key-in-d-keys-finish-in-on-time-while-key-in-d-finishes-in-o1 ,并且可能是这个的副本 (请参阅Python 3相关部分: https://dev59.com/hGMm5IYBdhLWcg3wivb7)。 - Chris_Rands
1个回答

11
使用 dictionary.keys() 速度较慢,因为它做了更多的工作:
  • 它添加了一个属性查找;dictionary.keys
  • 它添加了一个方法调用(keys()),需要将当前调用帧推送到堆栈上,并在此后弹出。
  • 另一个对象必须被创建为返回值(字典视图)。它是一个轻量级的对象,但你仍然需要在堆上为它分配内存。
这些都是不必要的,因为针对字典和键测试的字典视图的包含测试测试了完全相同的内容。直接在字典上进行包含测试不包括值,在两种情况下都只测试键。
dict() 对象文档 中可以看到:

key in d
如果 d 中有键 key,则返回 True,否则返回 False。

请注意,使用时钟时间来测试性能差异并不是一个很好的方法。请改用 timeit 模块,该模块选择最佳执行计时器,禁用 GC 以消除偏差源,并重复测试多次以最小化系统偏差。
您可以通过单独测试上述附加步骤(将调用和对象创建合并为一个步骤)来重现时间差异。默认情况下,timeit.timeit() 重复测试 100 万次,并返回所花费的总时间:
>>> import timeit
>>> from string import ascii_lowercase
>>> d = {l: i for i, l in enumerate(ascii_lowercase)}
>>> 't' in d
True
>>> timeit.timeit('d.keys', globals={'d': d})
0.0439452639548108
>>> timeit.timeit('keys()', globals={'keys': d.keys})
0.06267352704890072

仅仅查找 .keys 属性 100万次就需要44毫秒,而调用该方法(不包含属性查找)又增加了63毫秒。但是这两种方法都需要查找全局名称的开销:

>>> timeit.timeit('d', globals={'d': d})
0.027833244064822793

因此,人们预计这两种方法之间会有107-28 == 79毫秒(大约)的差异。

实际上,使用't' in d't' in d.keys()之间的时间差大约就是这么多:

>>> timeit.timeit('"t" in d.keys()', globals={'d': d})
0.11647015693597496
>>> timeit.timeit('"t" in d', globals={'d': d})
0.0370339349610731

116 - 37等于79毫秒,与预测相符。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接