Влияние вида импорта на скорость исполнения кода
Вспомним, на что и как, кроме читаемости, влияет вид импорта.
В руководстве по оформлению кода на Питоне для стандартной библиотеки (PEP 8) можно найти параграф, посвященный вариантам оформления импортов.
Из параграфа мы узнаём, что при использовании абсолютных импортов допускается как форма
Использую
Вариант 1. Импорт цепочки. В этом варианте будет рассматриваться поведение кода, подобного следующему:
Вариант 2. Импорт целевого модуля. В данном варианте импортируется модуль, либо создаётся для него псевдоним:
Вариант 3. Импорт атрибута из целевого модуля. Здесь импортируется только конкретный атрибут:
Проведём синтетический тест при помощи модуля
Для наглядности, я сократил результаты замеров времени до четырёх цифр после точки.
Разница между первым вариантом и последующими явственная.
Третий выигрывает у предыдущих. Почему так, давайте разберёмся.
Давайте взглянем, во что превратил нашу функцию
Вариант 1
Вариант 2
Вариант 3
Видно, что в третьем варианте инструкций меньше всего. В нём для вызова импортированной функции используется одна
Во втором варианте уже используется спарка
В первом варианте к
С каждой новой версией Питона скорость доступа к атрибутам разного уровня изменяется. Разработчики стараются улучшать этот показатель по мере возможности. Вот некоторые параметры (отсюда):
Если верить табличке, то, если мы возьмём наш вариант 3, где используется глобальный атрибут
Вариант 4
Припасём ссылочку на внешнюю функцию в параметре
В инструкциях при этом
Если вы надеетесь, что
Используйте тот вид импорта, который подходит вам в конкретной ситуации: не ухудшает читаемость и производительность. Это касается, не только абсолютных импортов, но и относительных, которыми некоторые пренебрегают.
А вы экономите на спичках?
Из параграфа мы узнаём, что при использовании абсолютных импортов допускается как форма
from a.b import x
(занос в область видимости атрибута x
), так и import a.b.x
(занос цепочки; может использоваться в случаях, когда имя x
конфликтует с таким же именем, объявленным в текущем модуле). Кажется тут всё понятно и нет ничего примечательного. Однако давайте взглянем повнимательнее, чем будут отличаться вызовы при этих видах импорта. Использую
Python 3.8.5 (default, Jul 28 2020, 12:59:40) [GCC 9.3.0] on linux
.Вариант 1. Импорт цепочки. В этом варианте будет рассматриваться поведение кода, подобного следующему:
import some.other.there.here
def run(): some.other.there.here.out() # кажется перебор, но и так пишут
Вариант 2. Импорт целевого модуля. В данном варианте импортируется модуль, либо создаётся для него псевдоним:
from some.other.there import here
# либо import some.other.there.here as here
def run(): here.out()
Вариант 3. Импорт атрибута из целевого модуля. Здесь импортируется только конкретный атрибут:
from some.other.there.here import out
def run(): out()
На заметку
Вспомни: каждое разрешение атрибута (каждая точка в выражениях типа
a.b.c.d.x
) будет тебе чего-то стоить.Замер скорости исполнения
Проведём синтетический тест при помощи модуля
timeit
для различных вариантов импорта. # вариант 1
src = 'import some.other.there.here\ndef run(): some.other.there.here.out()'
# [0.2058, 0.1854, 0.1825, 0.1832, 0.1809]
# вариант 2
src = 'from some.other.there import here\ndef run(): here.out()'
# [0.1361, 0.1240, 0.1246, 0.1220, 0.1197]
# вариант 3
src = 'from some.other.there.here import out\ndef run(): out()'
# [0.1251, 0.1032, 0.1007, 0.1054, 0.1021]
# код профилирования
# import timeit
# print(timeit.Timer('run()', setup=src).repeat())
Для наглядности, я сократил результаты замеров времени до четырёх цифр после точки.
Разница между первым вариантом и последующими явственная.
Третий выигрывает у предыдущих. Почему так, давайте разберёмся.
Инструкции
Давайте взглянем, во что превратил нашу функцию
run()
компилятор: from dis import dis
dis(run)
Вариант 1
6 0 LOAD_GLOBAL 0 (some)
2 LOAD_ATTR 1 (other)
4 LOAD_ATTR 2 (there)
6 LOAD_ATTR 3 (here)
8 LOAD_METHOD 4 (out)
10 CALL_METHOD 0
12 POP_TOP
14 LOAD_CONST 0 (None)
16 RETURN_VALUE
Вариант 2
6 0 LOAD_GLOBAL 0 (here)
2 LOAD_METHOD 1 (out)
4 CALL_METHOD 0
6 POP_TOP
8 LOAD_CONST 0 (None)
10 RETURN_VALUE
Вариант 3
6 0 LOAD_GLOBAL 0 (out)
2 CALL_FUNCTION 0
4 POP_TOP
6 LOAD_CONST 0 (None)
8 RETURN_VALUE
Видно, что в третьем варианте инструкций меньше всего. В нём для вызова импортированной функции используется одна
CALL_FUNCTION
.Во втором варианте уже используется спарка
LOAD_METHOD
+ CALL_METHOD
, отъедая чуточку процессорного времени.В первом варианте к
LOAD_METHOD
+ CALL_METHOD
добавляются ещё три LOAD_ATTR
, добирающиеся до нужного атрибута перед тем как его использовать.Внимание
Значит ли описанное выше, что вам нужно срочно бежать и переписывать код? В подавляющем большинстве случаев — нет. Тем более, если это не нагруженные выполняющиеся часто функции. Но помнить о том, что каждая точка вам чего-то стоит не помешает.
С каждой новой версией Питона скорость доступа к атрибутам разного уровня изменяется. Разработчики стараются улучшать этот показатель по мере возможности. Вот некоторые параметры (отсюда):
Python version 3.4 3.5 3.6 3.7 3.8 3.9
-------------- --- --- --- --- --- ---
локальная 7.1 7.1 5.4 5.1 3.9 3.9
нелокальная 7.1 8.1 5.8 5.4 4.4 4.5
глобальная 15.5 19.0 14.3 13.6 7.6 7.8
встроенная 21.1 21.6 18.5 19.0 7.5 7.8
аттр_класса_из_класса 25.6 26.5 20.7 19.5 18.4 17.9
аттр_класса_из_экземпляра 22.8 23.5 18.8 17.1 16.4 16.9
аттр_экземпляра 32.4 33.1 28.0 26.3 25.4 25.3
аттр_экземпляра_слоты 27.8 31.3 20.8 20.8 20.2 20.5
именованный_кортеж 73.8 57.5 45.0 46.8 18.4 18.7
связанный_метод 37.6 37.9 29.6 26.9 27.7 41.1
Если верить табличке, то, если мы возьмём наш вариант 3, где используется глобальный атрибут
out
и сделаем его локальным для функции, то можно ещё чуть ускориться. Давайте проверим. Сделать out
локальной переменной можно при помощи хитрости, объявив её значением по умолчанию параметра функции (тем самым закешировав и выделив ей место на стеке). Вариант 4
Припасём ссылочку на внешнюю функцию в параметре
out_
, а потом используем внутри нашей функции. from some.other.there.here import out
def run(out_=out): out_()
В инструкциях при этом
LOAD_GLOBAL
сменится на LOAD_FAST
: 6 0 LOAD_FAST 0 (out_)
2 CALL_FUNCTION 0
4 POP_TOP
6 LOAD_CONST 0 (None)
8 RETURN_VALUE
Если вы надеетесь, что
LOAD_FAST
магическим образом вдруг поменяет дело в корне, то не стоит. На четырёх цифрах после точки разница неощутима тем более: src = 'from some.other.there.here import out\ndef run(out_=out): out_()'
# [0.1107, 0.1001 0.1005, 0.1002, 0.1018]
Используйте тот вид импорта, который подходит вам в конкретной ситуации: не ухудшает читаемость и производительность. Это касается, не только абсолютных импортов, но и относительных, которыми некоторые пренебрегают.
А вы экономите на спичках?
Категории
Область
Интерпретатор
Уровень
На заметку
Читайте нас в Twitter. Ссылка в самом низу страницы.