Что можно сделать для ускорения работы функции
Что проверяют
Начинаете ли вы с замера или сразу с догадок. В задачах с данными это критично: интуиция про узкое место ошибается почти всегда, а цена ошибки это переписанная функция, которая занимала два процента времени.
Как отвечать
Разложить по порядку действий.
- Измерить:
cProfileна вызов целиком,line_profilerпострочно,tracemallocпо памяти,timeitна микрозамеры. Без профиля оптимизация это угадывание. - Алгоритм и объём данных: убрать поиск по списку внутри цикла в пользу
setилиdict, фильтровать и агрегировать до тяжёлой части, не читать лишние столбцы. - Векторизация: заменить цикл по строкам и
applyна операции numpy и pandas над столбцами, подобрать типы, использовать категориальные значения, читать parquet вместо csv, при больших объёмах смотреть на Polars или DuckDB. - Кэш:
functools.lru_cacheдля чистых функций, сохранение промежуточных результатов на диск, чтобы не пересчитывать при каждом запуске. - Параллелизм по типу нагрузки: вычисления упираются в GIL, значит процессы,
joblibили распределённый движок; ожидание сети и диска лечится потоками и асинхронностью. - Ускорители: numba, Cython, библиотеки на C, для моделей батчи и GPU.
Закончить стоит критерием остановки: оптимизация заканчивается, когда достигнут нужный бюджет времени, а читаемость кода тоже ресурс.
Где спотыкаются
Оптимизируют без профилирования. Переписывают красивое место, получают проценты, а основное время уходило на чтение файла или на повторный запрос в базу внутри цикла.
Обещают ускорение вычислений за счёт потоков. Пока код исполняет питоновский байт-код, глобальная блокировка не даст выполняться параллельно, выигрыш дают процессы или библиотеки, отпускающие блокировку внутри своих операций.
Кэшируют всё подряд. lru_cache требует хэшируемых аргументов, отдаёт устаревший результат для функции, которая ходит в базу, а без ограничения размера растит память, что в пайплайне с большими объектами кончается падением по памяти вместо ускорения.