Оптимизация загрузки скриптов.
2022.11.10
Для использования сторонних пакетов — и своих тоже — в python’е используется конструкция import
:
Все import
ы, по умолчанию, выполняются во время загрузки скриптов. Это не всегда хорошо: представим себе CLI-утилиту с двумя командами:
$ ./utility.py --help
./utility.py usage:
* --help Shows this message
* hard_work Does all the work
$ ./utility.py hard_work
Importing the universe...
Looking for stars*...
Done!
Первая команда показывает документацию, вторая — делает что-то полезное. Если для одной из команды требуются некие импорты (возможно тяжёлые), они будут выполняться для всех команд, даже если они там не нужны. Это сказывается на времени запуска скрипта.
Посмотреть, какие импорты выполняются при запуске скрипта можно с помощью встроенного профайлера:
$ PYTHONPROFILEIMPORTTIME=1 ./utility.py --help
import time: self [us] | cumulative | imported package
import time: 1000 | 1000 | the_universe
import time: 2000 | 2000 | stars
...
Для чтения таких файликов рекомендую использовать tuna, визуализатор профайлов:
Что делать с такими импортами, которые нужны не всегда? Использовать ленивые импорты! На эту тему уже расписан PEP 690, если хотите действительно полезной информации, читайте его.
Ленивые импорты можно (и нужно) поддерживать нативно, на уровне инерпретатора, чтобы была возможность учитывать всю необходимую семантику. Такое уже умеет cinder, экспериментальный форк python’а от facebook. В нём есть и другие интересные оптимизации, см. readme.md#whats-here.
Заготовка для поддержки ленивых импортов есть в importlib:
import importlib.util
import sys
def lazy_import(name):
spec = importlib.util.find_spec(name)
loader = importlib.util.LazyLoader(spec.loader)
spec.loader = loader
module = importlib.util.module_from_spec(spec)
sys.modules[name] = module
loader.exec_module(module)
return module
lazy_typing = lazy_import("typing")
#lazy_typing is a real module object,
#but it is not loaded in memory yet.
lazy_typing.TYPE_CHECKING
False
Попробуем переписать наши импорты на ленивые:
Теперь, модули universe
и stars
будут импортированы только при обращении к их членам:
В некоторых случаях, когда модули нужны не всегда, это поможет ускорить запуск скрипта. Однако бездумно раскидывать ленивые импорты не хорошо:
lazy_import("")
выглядит хуже, чем import
;Более “нативно-выглядящая” поддержка может быть реализована с помощью подмены импортера, как сделано в py-demandimport:
import demandimport; demandimport.enable()
# Imports of the following form will be delayed
import a, b.c
import a.b as c
from a import b, c # a will be loaded immediately, though
Здесь основной минус — это monkeypatching в красивой обёртке, что всегда нехорошо.