Прямой ответ
Профилирование отвечает на вопрос «где именно медленно/прожорливо» измерениями, а не догадками. Минимальный набор инструментов встроен или ставится за минуту: cProfile (CPU по функциям, стандартная библиотека), snakeviz (визуализация отчёта), line_profiler (построчно внутри функции), memory_profiler (рост памяти по строкам). Правило практики: сначала cProfile → находим 2–3 функции, дающие 80% времени → внутри них line_profiler → оптимизируем только их. Оптимизация без профиля — это переписывание наугад.
Шаг 1. CPU-профиль целиком
python -m cProfile -o prof.out main.py
snakeviz prof.out # интерактивная визуализация в браузере
Или из кода/ноутбука:
import cProfile, pstats
cProfile.run("main()", "prof.out")
p = pstats.Stats("prof.out"); p.sort_stats("cumulative").print_stats(20)
Как читать: tottime — время в самой функции (без вызовов), cumtime — с вызовами. Ищите функции с большим tottime и высоким ncalls (миллионы вызовов мелкой функции — классическое узкое место). Отдельный сигнал: огромное время в read/recv — это ожидание I/O, и лечить его нужно не кодом, а батчингом/кэшем.
Шаг 2. Построчный разбор горячей функции
pip install line_profiler
kernprof -l -v slow_module.py # @profile на нужной функции
Типовые находки в Python-коде:
- повторные вычисления в цикле (регулярные выражения, парсинг конфигурации) — выносятся наружу;
- поиск в списке
if x in big_list— замена наsetдаёт ускорение в сотни раз; - конкатенация строк в цикле —
''.join(parts)вместо+=; - частые мелкие обращения к атрибутам/индексам в горячем цикле — локальная переменная;
datetime.strptime/json.loadsвнутри цикла по миллионам строк — кэширование или векторизация.
Шаг 3. Память: где течёт и что пикует
from memory_profiler import profile
@profile
def process():
...
Запуск покажет прирост по каждой строке. Три типовых сценария:
- Пик в одной операции — обычно промежуточная копия данных (pandas
merge/apply); лечится чанками и выбором колонок, приёмы — в статье про Jupyter и большие данные. - Монотонный рост — коллекция-аккумулятор без очистки, кэш без ограничения (
functools.lru_cache(maxsize=...)— с ограничением!), подписки на события без отписки. - Рост в долгоживущем сервисе — снимки объектов через
tracemalloc(стандартная библиотека):
import tracemalloc
tracemalloc.start()
# ... работа ...
for stat in tracemalloc.take_snapshot().statistics("lineno")[:10]:
print(stat)
Интерпретация: что делать с результатами
- Профилировщик добавляет накладные расходы (cProfile — до десятков процентов); относительные доли важнее абсолютных миллисекунд.
- Профилируйте на реалистичных данных: на 100 строках узкое место может не проявиться.
- После каждой оптимизации — повторный профиль: иногда «ускорение» съедается другим местом, а иногда оптимизированная функция давала лишь 3%.
- Порядок рычагов обычно такой: алгоритм/структуры данных → батчинг и кэш → векторизация (numpy/polars) → параллелизм (
concurrent.futures) → и только потом экзотика (Cython, компиляторы).
Профилирование в IDE и CI-гигиена
PyCharm/VS Code имеют встроенные профайлеры с тем же cProfile под капотом — удобно для быстрого цикла «правка → прогон»; бюджеты памяти IDE при этом учитывайте по статье про IDE и ОЗУ. Для команды полезно: регрессионные замеры времени критичного пути в CI (pytest-benchmark), чтобы замедление ловилось коммитом, а не жалобой пользователя. Окружение для замеров — фиксированное (lock-файл), иначе сравниваете версии библиотек, а не код; устройство окружений — в материале про venv/conda/uv.
Разбор типового кейса: «скрипт работает 40 минут»
Показательный сценарий, повторяющийся от проекта к проекту. Скрипт обработки CSV на 2 млн строк выполняется 40 минут; интуиция подсказывает «мало памяти, нужен сервер». Профиль показывает другое:
cProfile→ 62%tottimeвpandas.core.frame.applyпострочного вызова, 18% — вjson.loadsвнутри цикла, 9% — чтение файла.line_profilerпо функции-обработчику:apply(axis=1)вызывает Python-функцию 2 млн раз — это и есть узкое место, а не память.- Правки: построчный
applyзаменён на векторизованные операции (str-аксессоры,np.where), парсинг JSON вынесен в один проход с кэшем по уникальным значениям, чтение —usecols+dtype. - Результат: 40 минут → 3,5 минуты на том же ноутбуке, потребление памяти почти не изменилось.
Мораль кейса: до профилирования «тормоза» объясняли железом, после — алгоритмом. Апгрейд ОЗУ не ускорил бы этот скрипт ни на минуту. Обратная ситуация тоже встречается: профиль показывает честный CPU-bound в численных расчётах — тогда помогают векторизация/компиляция или действительно более мощный процессор.
Порядок принятия решения общий: сначала измерение, потом покупка. Если профиль показывает I/O-ожидание — смотрите на диск и сеть; если свопинг — на память; если CPU — на алгоритм. Методику измерения потребления памяти системой см. в статье про IDE и ОЗУ, инструменты построчного разбора памяти — выше по тексту.
FAQ
cProfile искажает результаты — чем заменить для точности?
Семплирующие профайлеры (py-spy) не замедляют процесс и подходят для продакшн-диагностики: py-spy top --pid <pid>. Для офлайн-разбора cProfile точнее по числу вызовов.
Как профилировать асинхронный код? cProfile видит корутины как функции, но ожидание событий искажает картину; используйте специализированные инструменты (yappi с режимом async) и смотрите на wall-time этапов.
Стоит ли профилировать в Jupyter?
Да: %prun (cProfile) и %memit работают прямо в ячейке — быстрый способ найти горячую точку до переноса пайплайна в скрипт.
Профиль показал, что 60% времени — json.loads. Что делать? Смотрите, откуда данные: повторяющийся парсинг одного и того же — кэшируйте; большие объёмы — переходите на более быстрые форматы (parquet, msgpack) или библиотеки-парсеры (orjson).
Как профилировать веб-сервис под нагрузкой, а не скрипт?
Поднимите локальную нагрузку (hey/locust/k6) и снимайте семплы py-spy параллельно: py-spy record -o flame.svg --pid <pid>. Flamegraph покажет горячие пути под реальным конкурентным профилем — часто они отличаются от однопоточного прогона cProfile.
Профиль показывает много времени в блокировках БД. Это код или база?
Смотрите долю: если recv/read сокета доминируют — запросы долгие, и начинать нужно с EXPLAIN и индексов на стороне БД, а не с Python. Код-сайд поможет кэш и батчинг запросов (N+1 — классика ORM).
Отдельная дисциплина — фиксация результатов: заведите в репозитории файл PERF.md (или тег в issue) с датой, версией кода, железом и ключевыми цифрами профиля. Через месяц оптимизаций только так можно показать «было 40 минут — стало 3,5», а не спорить по ощущениям. Для команды этот файл быстро становится источником правды о том, где узкое место системы на самом деле.
Источники: документация Python (profile/tracemalloc), line_profiler, memory_profiler (ссылки в шапке).