Вспомним, на что и как, кроме читаемости, влияет вид импорта.
В руководстве по оформлению кода на Питоне для стандартной библиотеки (PEP 8) можно найти параграф, посвященный вариантам оформления импортов.

Из параграфа мы узнаём, что при использовании абсолютных импортов допускается как форма 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. Ссылка в самом низу страницы.