Как интерпретатор Python обрабатывает строку с логированием результата функции и почему это может быть дорого по ресурсам
Что проверяют
Знаете ли вы, что аргументы функции в Python вычисляются до её вызова, а logging откладывает только форматирование, но не вычисления. Следом обычно просят переписать строку так, чтобы при выключенном DEBUG она почти ничего не стоила.
Как отвечать
Разберите строку logger.debug(f"Result: {compute(data)}") по шагам:
- вызов
compute(data): выполняется всегда, независимо от уровня логгера, потому что это аргумент; - сборка f-строки: у результата вызывается
__format__(для большинства объектов этоstr()), создаётся новая строка. Для большого словаря или датафрейма это мегабайты и заметное время; - проверка уровня: только теперь
Logger.debugвызываетisEnabledFor, видит, чтоDEBUGвыключен, и выбрасывает готовую строку. Работа сделана впустую; - если уровень включён: создаётся
LogRecordс именем потока и процесса,findCallerпроходит по стеку в поисках места вызова, затем каждый хендлер берёт блокировку и синхронно пишет в файл или stdout. На горячем пути это уже I/O.
Как исправлять:
- ленивое форматирование:
logger.debug("Result: %s", result). Строка соберётся, только если запись реально уйдёт в хендлер; - охрана дорогих вычислений:
if logger.isEnabledFor(logging.DEBUG):вокруг тяжёлого вызова,%sего не спасёт; - вынос записи из потока:
QueueHandlerиQueueListener, чтобы медленный диск или сеть не тормозили обработку запросов; - линтеры: pylint предупреждает о f-строках в логировании (
logging-fstring-interpolation), в ruff есть такое же правило в группеG.
Где спотыкаются
Считают, что %s-стиль делает ленивым всё выражение. Нет: logger.debug("%s", compute()) по-прежнему вызывает compute(), откладывается лишь подстановка в шаблон.
Советуют f-строки как «самый быстрый способ форматирования» и для логов тоже. В обычном коде это верно, но в логировании f-строка собирается всегда, даже когда сообщение никто не увидит.
Уходят в рассуждения про сборщик мусора и фрагментацию памяти от строк. Реальная цена почти всегда в лишнем вызове функции, тяжёлом __repr__ и синхронной записи, а не в самом объекте строки.