Python 为什么创建字典时,这个循环比字典理解快?

Python 为什么创建字典时,这个循环比字典理解快?,python,python-3.x,performance,python-internals,dictionary-comprehension,Python,Python 3.x,Performance,Python Internals,Dictionary Comprehension,我没有软件/计算机科学背景,但我喜欢用Python编写代码,并且通常能够理解为什么事情会更快。我真的很想知道为什么这个for循环运行得比字典理解快。有什么见解吗 问题:给定一个包含这些键和值的字典a,返回一个值为键、键为值的字典。(挑战:在一行中完成此操作) 代码呢 a = {'a':'hi','b':'hey','c':'yo'} b = {} for i,j in a.items(): b[j]=i %% timeit 932 ns ± 37.2 ns per loop b =

我没有软件/计算机科学背景,但我喜欢用Python编写代码,并且通常能够理解为什么事情会更快。我真的很想知道为什么这个for循环运行得比字典理解快。有什么见解吗

问题:给定一个包含这些键和值的字典
a
,返回一个值为键、键为值的字典。(挑战:在一行中完成此操作)

代码呢

a = {'a':'hi','b':'hey','c':'yo'}

b = {}
for i,j in a.items():
    b[j]=i

%% timeit 932 ns ± 37.2 ns per loop

b = {v: k for k, v in a.items()}

%% timeit 1.08 µs ± 16.4 ns per loop

您的测试输入太小;虽然与列表理解相比,字典理解相对于
for
循环没有太多的性能优势,但对于实际的问题大小,它可以而且确实击败
for
循环,尤其是在针对全局名称时

您的输入仅由3个键值对组成。相反,使用1000个元素进行测试,我们发现计时非常接近:

>>> import timeit
>>> from random import choice, randint; from string import ascii_lowercase as letters
>>> looped = '''\
... b = {}
... for i,j in a.items():
...     b[j]=i
... '''
>>> dictcomp = '''b = {v: k for k, v in a.items()}'''
>>> def rs(): return ''.join([choice(letters) for _ in range(randint(3, 15))])
...
>>> a = {rs(): rs() for _ in range(1000)}
>>> len(a)
1000
>>> count, total = timeit.Timer(looped, 'from __main__ import a').autorange()
>>> (total / count) * 1000000   # microseconds per run
66.62004760000855
>>> count, total = timeit.Timer(dictcomp, 'from __main__ import a').autorange()
>>> (total / count) * 1000000   # microseconds per run
64.5464928005822
不同之处在于,dict comp速度更快,但仅限于此规模。由于键值对的数量是原来的100倍,因此差异要大一些:

>>> a = {rs(): rs() for _ in range(100000)}
>>> len(a)
98476
>>> count, total = timeit.Timer(looped, 'from __main__ import a').autorange()
>>> total / count * 1000  # milliseconds, different scale!
15.48140200029593
>>> count, total = timeit.Timer(dictcomp, 'from __main__ import a').autorange()
>>> total / count * 1000  # milliseconds, different scale!
13.674790799996117

这不是很大的差异,当你考虑两个处理近100K键值对。不过,for的

循环显然要慢一些

那么为什么3个元素的速度差呢?因为理解(字典、集合、列表理解或生成器表达式)是作为一个新函数实现的,调用该函数有一个基本成本,普通循环不需要支付

下面是两个备选方案的字节码反汇编;请注意,对于dict理解,顶级字节码中的
MAKE_函数
CALL_函数
操作码有一个单独的部分说明该函数的作用,实际上这两种方法之间几乎没有区别:

>>> import dis
>>> dis.dis(looped)
  1           0 BUILD_MAP                0
              2 STORE_NAME               0 (b)

  2           4 SETUP_LOOP              28 (to 34)
              6 LOAD_NAME                1 (a)
              8 LOAD_METHOD              2 (items)
             10 CALL_METHOD              0
             12 GET_ITER
        >>   14 FOR_ITER                16 (to 32)
             16 UNPACK_SEQUENCE          2
             18 STORE_NAME               3 (i)
             20 STORE_NAME               4 (j)

  3          22 LOAD_NAME                3 (i)
             24 LOAD_NAME                0 (b)
             26 LOAD_NAME                4 (j)
             28 STORE_SUBSCR
             30 JUMP_ABSOLUTE           14
        >>   32 POP_BLOCK
        >>   34 LOAD_CONST               0 (None)
             36 RETURN_VALUE
>>> dis.dis(dictcomp)
  1           0 LOAD_CONST               0 (<code object <dictcomp> at 0x11d6ade40, file "<dis>", line 1>)
              2 LOAD_CONST               1 ('<dictcomp>')
              4 MAKE_FUNCTION            0
              6 LOAD_NAME                0 (a)
              8 LOAD_METHOD              1 (items)
             10 CALL_METHOD              0
             12 GET_ITER
             14 CALL_FUNCTION            1
             16 STORE_NAME               2 (b)
             18 LOAD_CONST               2 (None)
             20 RETURN_VALUE

Disassembly of <code object <dictcomp> at 0x11d6ade40, file "<dis>", line 1>:
  1           0 BUILD_MAP                0
              2 LOAD_FAST                0 (.0)
        >>    4 FOR_ITER                14 (to 20)
              6 UNPACK_SEQUENCE          2
              8 STORE_FAST               1 (k)
             10 STORE_FAST               2 (v)
             12 LOAD_FAST                1 (k)
             14 LOAD_FAST                2 (v)
             16 MAP_ADD                  2
             18 JUMP_ABSOLUTE            4
        >>   20 RETURN_VALUE
超过0.1μs来创建一个带有一个参数的函数对象,并调用它(对于传入的
None
值,使用额外的
LOAD_CONST
)!这就是3个键值对的循环计时和理解计时之间的差异

你可以把这比作一个人用铲子挖一个小洞比反铲挖得快而感到惊讶。反铲当然可以挖得很快,但是如果你需要先启动反铲并将其移动到位,那么一个带铲的人可以更快地开始工作

除了几个键值对(挖一个更大的洞),函数create和call cost逐渐消失为虚无。此时,dict理解和显式循环基本上做了相同的事情:

  • 取下一个键值对,将它们放在堆栈上
  • 通过字节码操作调用
    dict.\uuuuuu setitem\uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu
这与列表理解不同,在列表理解中,普通循环版本必须使用
list.append()
,包括属性查找和每个循环迭代的函数调用。列表理解速度的优势来自此差异;请参阅

dict理解所增加的是,在将
b
绑定到最终字典对象时,只需查找一次目标字典名称。如果目标字典是全局变量而不是局部变量,则理解将获胜,这是显而易见的:

>>> a = {rs(): rs() for _ in range(1000)}
>>> len(a)
1000
>>> namespace = {}
>>> count, total = timeit.Timer(looped, 'from __main__ import a; global b', globals=namespace).autorange()
>>> (total / count) * 1000000
76.72348440100905
>>> count, total = timeit.Timer(dictcomp, 'from __main__ import a; global b', globals=namespace).autorange()
>>> (total / count) * 1000000
64.72114819916897
>>> len(namespace['b'])
1000

因此,只需使用dict理解。要处理的<30个元素之间的差异太小,根本不值得关注,而且当您生成一个全局项或有更多项时,dict理解就会胜出。

从某种意义上说,这个问题与我很久以前回答的问题非常相似。然而,这种行为导致很明显,你的字典太小了,无法克服创建新函数框架和在堆栈中推/拉它的成本。为了更好地理解这一点,让我们来看看你的两个代码片段:

In [1]: a = {'a':'hi','b':'hey','c':'yo'}
   ...: 
   ...: def reg_loop(a):
   ...:     b = {}
   ...:     for i,j in a.items():
   ...:         b[j]=i
   ...:         

In [2]: def dict_comp(a):
   ...:     b = {v: k for k, v in a.items()}
   ...:     

In [3]: 

In [3]: %timeit reg_loop(a)
529 ns ± 7.89 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

In [4]: 

In [4]: %timeit dict_comp(a)
656 ns ± 5.39 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

In [5]: 

In [5]: import dis

In [6]: dis.dis(reg_loop)
  4           0 BUILD_MAP                0
              2 STORE_FAST               1 (b)

  5           4 SETUP_LOOP              28 (to 34)
              6 LOAD_FAST                0 (a)
              8 LOAD_METHOD              0 (items)
             10 CALL_METHOD              0
             12 GET_ITER
        >>   14 FOR_ITER                16 (to 32)
             16 UNPACK_SEQUENCE          2
             18 STORE_FAST               2 (i)
             20 STORE_FAST               3 (j)

  6          22 LOAD_FAST                2 (i)
             24 LOAD_FAST                1 (b)
             26 LOAD_FAST                3 (j)
             28 STORE_SUBSCR
             30 JUMP_ABSOLUTE           14
        >>   32 POP_BLOCK
        >>   34 LOAD_CONST               0 (None)
             36 RETURN_VALUE

In [7]: 

In [7]: dis.dis(dict_comp)
  2           0 LOAD_CONST               1 (<code object <dictcomp> at 0x7fbada1adf60, file "<ipython-input-2-aac022159794>", line 2>)
              2 LOAD_CONST               2 ('dict_comp.<locals>.<dictcomp>')
              4 MAKE_FUNCTION            0
              6 LOAD_FAST                0 (a)
              8 LOAD_METHOD              0 (items)
             10 CALL_METHOD              0
             12 GET_ITER
             14 CALL_FUNCTION            1
             16 STORE_FAST               1 (b)
             18 LOAD_CONST               0 (None)
             20 RETURN_VALUE
[1]中的
a={'a':'hi','b':'hey','c':'yo'}
...: 
…:def reg_循环(a):
…:b={}
…:对于a.items()中的i,j:
…:b[j]=i
...:         
在[2]中:定义dict_comp(a):
…:b={v:k代表k,v在a.items()中]
...:     
在[3]中:
在[3]:%timeit reg_循环(a)
每个回路529纳秒±7.89纳秒(7次运行的平均值±标准偏差,每个1000000个回路)
在[4]中:
在[4]中:%timeit dict__comp(a)
每个回路656纳秒±5.39纳秒(7次运行的平均值±标准偏差,每个1000000个回路)
在[5]中:
在[5]中:导入dis
In[6]:dis.dis(reg_循环)
4 0生成映射0
2商店1(b)
5 4设置环路28(至34)
6加载速度0(a)
8加载方法0(项目)
10调用方法0
12获取ITER
>>国际热核聚变实验堆16的14(至32)
16拆包顺序2
18商店2(一)
20商店快速3(j)
6 22快速加载2(i)
24负载快速1(b)
26负载快速3(j)
28存储单元
30跳/绝对14
>>32波普卢街区
>>34负载常数0(无)
36返回值
在[7]中:
In[7]:dis.dis(dict_comp)
2 0加载常数1(<0x7fbada1adf60处的代码对象dictcomp,文件“ipython-input-2-aac022159794”,第2行>)
2加载常数2(‘指令组件’)
4生成函数0
6加载速度0(a)
8加载方法0(项目)
10调用方法0
12获取ITER
14调用函数1
16
In [1]: a = {'a':'hi','b':'hey','c':'yo'}
   ...: 
   ...: def reg_loop(a):
   ...:     b = {}
   ...:     for i,j in a.items():
   ...:         b[j]=i
   ...:         

In [2]: def dict_comp(a):
   ...:     b = {v: k for k, v in a.items()}
   ...:     

In [3]: 

In [3]: %timeit reg_loop(a)
529 ns ± 7.89 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

In [4]: 

In [4]: %timeit dict_comp(a)
656 ns ± 5.39 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

In [5]: 

In [5]: import dis

In [6]: dis.dis(reg_loop)
  4           0 BUILD_MAP                0
              2 STORE_FAST               1 (b)

  5           4 SETUP_LOOP              28 (to 34)
              6 LOAD_FAST                0 (a)
              8 LOAD_METHOD              0 (items)
             10 CALL_METHOD              0
             12 GET_ITER
        >>   14 FOR_ITER                16 (to 32)
             16 UNPACK_SEQUENCE          2
             18 STORE_FAST               2 (i)
             20 STORE_FAST               3 (j)

  6          22 LOAD_FAST                2 (i)
             24 LOAD_FAST                1 (b)
             26 LOAD_FAST                3 (j)
             28 STORE_SUBSCR
             30 JUMP_ABSOLUTE           14
        >>   32 POP_BLOCK
        >>   34 LOAD_CONST               0 (None)
             36 RETURN_VALUE

In [7]: 

In [7]: dis.dis(dict_comp)
  2           0 LOAD_CONST               1 (<code object <dictcomp> at 0x7fbada1adf60, file "<ipython-input-2-aac022159794>", line 2>)
              2 LOAD_CONST               2 ('dict_comp.<locals>.<dictcomp>')
              4 MAKE_FUNCTION            0
              6 LOAD_FAST                0 (a)
              8 LOAD_METHOD              0 (items)
             10 CALL_METHOD              0
             12 GET_ITER
             14 CALL_FUNCTION            1
             16 STORE_FAST               1 (b)
             18 LOAD_CONST               0 (None)
             20 RETURN_VALUE