Вы тестируете со слишком маленьким входом;в то время как понимание словаря не имеет такого большого преимущества в производительности по сравнению с циклом 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
, которая не , а большая разница, если учесть, что обе обработанные пары ключей-значений составляют почти 100 тыс.Тем не менее, цикл for
явно медленнее .
Так почему же разница в скорости с 3 элементами?Поскольку понимание (словарь, набор, списочные выражения или выражение генератора) находится под капотом, реализованным как новая функция , и вызов этой функции имеет базовую стоимость, которую простой цикл не должен платить.
Вот разборка для байт-кода для обеих альтернатив;обратите внимание на коды операций MAKE_FUNCTION
и CALL_FUNCTION
в байт-коде верхнего уровня для понимания dict, есть отдельный раздел о том, что тогда делает эта функция, и здесь на самом деле очень мало различий между двумя подходами:
>>> 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
Существенные различия: зацикленный код использует LOAD_NAME
для b
каждой итерации и STORE_SUBSCR
для хранения пары ключ-значение в загруженном dict.Понимание словаря использует MAP_ADD
для достижения того же, что и STORE_SUBSCR
, но не обязательно каждый раз загружать это имя b
.
Но с 3 итерациями комбинация MAKE_FUNCTION
/ CALL_FUNCTION
, которую должно выполнить понимание dict, является настоящим тормозом исполнения:
>>> make_and_call = '(lambda i: None)(None)'
>>> dis.dis(make_and_call)
1 0 LOAD_CONST 0 (<code object <lambda> at 0x11d6ab270, file "<dis>", line 1>)
2 LOAD_CONST 1 ('<lambda>')
4 MAKE_FUNCTION 0
6 LOAD_CONST 2 (None)
8 CALL_FUNCTION 1
10 RETURN_VALUE
Disassembly of <code object <lambda> at 0x11d6ab270, file "<dis>", line 1>:
1 0 LOAD_CONST 0 (None)
2 RETURN_VALUE
>>> count, total = timeit.Timer(make_and_call).autorange()
>>> total / count * 1000000
0.12945385499915574
Более 0,1 мкс, чтобы создать объект функции с одним аргументом и вызвать его (с дополнительным LOAD_CONST
для значения None
, которое мы передаем)!В этом и заключается разница между зацикленным временем и временем понимания для трех пар ключ-значение.
Можно сравнить это с удивлением, что человек с лопатой может вырыть маленькую яму быстрее, чем экскаватор.Экскаватор может, конечно, быстро копать, но человек с лопатой может начать быстрее, если вам нужно запустить экскаватор и сначала переместить его в положение!
За несколькими парами ключ-значение (рытье большую яму), функция создания и вызова стоимость исчезает в ничто.На этом этапе понимание dict и явный цикл в основном делают одно и то же:
- принимают следующую пару ключ-значение, вставляют их в стек
- вызовите хук
dict.__setitem__
с помощью операции байт-кода с двумя верхними элементами в стеке (либо STORE_SUBSCR
, либо MAP_ADD
. Это не считается "вызовом функции", поскольку все это внутренне обрабатывается в цикле интерпретатора.
Это отличается от понимания списка, когда версия простого цикла должна использовать list.append()
, включая поиск атрибутов, и вызов функции каждую итерацию цикла . СписокПреимущество в скорости понимания связано с этим различием, см. Понимание списка Python дорого
Что добавляет сложное понимание, так это то, что имя целевого словаря нужно искать только один раз, когда привязка b
к конечному объекту словаря. Если целевой словарь является global вместо локальной переменной, понимание выигрывает, руки вниз:
>>> 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
Так что просто используйте диктовку.Разница с <30 элементами для обработки слишком мала, чтобы о них заботиться, и в тот момент, когда вы генерируете глобал или имеете больше элементов, понимание диктата в любом случае побеждает. </p>