Python долго считает ответ: как читать cProfile и выбрать место для изменения

Запрос строит отчёт за несколько секунд. В обработчике всего три вызова, поэтому непонятно, какой из них менять. Разработчик начинает с самого длинного файла или заменяет цикл генератором, но время почти не меняется. Чтобы выбрать полезное изменение, сначала нужно связать время ответа с цепочкой функций и количеством работы. Для последовательного Python-кода это можно сделать через cProfile.

Начните с одного воспроизводимого расчёта

Возьмите функцию, которая строит отчёт из уже подготовленных данных. Зафиксируйте вход и проверьте ожидаемый результат. Уберите из первого опыта случайные обращения к сети и изменения базы: иначе каждый запуск может считать одно и то же разное время по внешней причине. Если медленная часть как раз ждёт зависимость, её задержку следует измерять отдельно по границам вызова, сохраняя связь с запросом.

В примере отчёт считает суммы по клиентам. Первая реализация для каждого клиента заново просматривает весь набор строк. Она проста и выдаёт правильные ответы, но объём работы растёт вместе с числом клиентов и строк. Нам нужно увидеть в профиле, что повторяется, затем заменить способ организации расчёта и сохранить результат.

Профиль рядом с проверкой правильности

import cProfile
import pstats
from collections import defaultdict

def total_for(customer, rows):
    return sum(amount for owner, amount in rows if owner == customer)

def report_slow(customers, rows):
    return {c: total_for(c, rows) for c in customers}

def report_once(customers, rows):
    totals = defaultdict(int)
    for owner, amount in rows:
        totals[owner] += amount
    return {c: totals[c] for c in customers}

customers = list(range(80))
rows = [(i % 80, i % 11) for i in range(16000)]
profiler = cProfile.Profile()
slow = profiler.runcall(report_slow, customers, rows)
fast = report_once(customers, rows)
assert slow == fast
assert sum(fast.values()) == sum(amount for _, amount in rows)
pstats.Stats(profiler).sort_stats("cumulative").print_stats(8)
print("customers:", len(fast), "total:", sum(fast.values()))

В отчёте найдите total_for и посмотрите ncalls. Вызовов будет столько же, сколько клиентов: 80. Затем проследите вызывающую функцию report_slow. Это объясняет причину повторного прохода, которую невозможно вывести только из числа секунд. report_once накапливает суммы за один проход. Проверка сравнивает оба словаря и общую сумму с исходными строками, поэтому изменение сохраняет смысл отчёта.

Два столбца времени отвечают на разные вопросы

Cumtime включает время функции вместе с вызванными ею функциями. Оно помогает найти дорогую ветку. Tottime показывает время внутри самой функции без её дочерних вызовов. Если обработчик стоит первым по cumtime, это ещё не означает, что внутри его нескольких строк есть тяжёлая операция: работа может происходить ниже. Откройте следующий уровень и найдите повторяемое действие. При сортировке по cumulative сначала исследуйте дорогую ветку, затем её дочерние вызовы.

Не складывайте cumtime родителя и ребёнка: один и тот же промежуток попадёт в сумму дважды. Не выбирайте функцию только по максимальному числу вызовов: очень частый, но дешёвый вызов может занимать небольшую долю работы. Полезная гипотеза соединяет оба наблюдения с устройством кода. В нашем случае для каждого клиента повторяется полный поиск по одним и тем же строкам.

Как подтвердить пользу изменения

Профиль помогает найти направление, но сам добавляет расходы. Сравнивать время двух реализаций лучше отдельным измерением на одинаковом входе, без включённого профилировщика. Повторите запуск несколько раз, учитывайте прогрев и сохраняйте размер данных. Измеряйте выигрыш на своём окружении и характерном входе, проверяя сохранение результата.

Затем проверьте границы задачи: клиент без строк должен получить ноль, неизвестные клиенты не должны неожиданно появиться в ответе, отрицательные корректировки должны суммироваться по вашим правилам. После перехода к новому способу накопления полезно расширить тесты именно этими примерами. Быстрая версия, которая изменила состав отчёта, не решает исходную задачу пользователя.

От локального расчёта к сервису

Верните функцию в обработчик и измерьте полный ответ. Если выигрыш растворился в ожидании базы или очереди, это новое знание о другой части пути. Выберите следующую границу измерения по фактической задержке. Для конкурентного сервиса один профиль также требует контекста: какие запросы выполнялись и какую нагрузку они создавали. Начальный последовательный опыт полезен тем, что позволяет объяснить одно изменение до перехода к общей картине.

Полезно хранить профиль до изменения вместе с командой воспроизведения. После правки число вызовов total_for должно исчезнуть из новой цепочки, а результат отчёта — сохраниться. Если вы всё ещё видите прежнюю функцию, убедитесь, что измеряется обновлённый код и тот же путь вызова. В сервисах с несколькими реализациями расчёта легко профилировать вспомогательный скрипт, тогда как HTTP-маршрут использует старую ветку. Связь между тестовым запуском и рабочим обработчиком должна быть проверяема по коду.

Если медленный участок выполняется внутри async-обработчика, проверьте блокирующие вызовы в цикле событий. А перед распараллеливанием разберите ограничение числа одновременных запросов, чтобы перенос ожидания не перегрузил зависимость.

Учиться выбирать инструмент по вопросу

В платном курсе «Python для профессионалов» отдельно рассматриваются cProfile и анализ результатов, timeit для измерения операций, профилирование памяти и приёмы оптимизации. Вы сможете последовательно выбирать дорогую ветку, проверять изменение и разбирать влияние устройства Python. Откройте программу курса и перейдите к разделу «Оптимизация и профилирование».

Продолжить на практике

  • Python для профессионалов ↗

    Python для профессионалов — курс для людей, стремящихся углубить свои знания языка. Вы изучите итераторы, генераторы, декораторы, современные возможности Python (3.8+), освоите метапрограммирование, асинхронность, внутреннее устройство интерпретатора, инструменты оптимизации и профилирования, а также получите уверенность в использовании продвинутых возможностей стандартной библиотеки.