Механизмы выполнения Python-программ
Принципы работы интерпретатора Python
Код Python обрабатывается интерпретатором в несколько этапов: лексический анализ, синтаксический разбор, формирование абстрактного синтаксического дерева (AST), компиляция в байт-код и выполнение в виртуальной машине. Для анализа производительности и поиска узких мест применяются специальные инструменты. Ниже рассмотрено наиболее эффективное решение и альтернативные подходы.
Как выявить узкие места в Python-программе?
Основной инструмент - встроенный профилировщик cProfile. Он записывает время вызова каждой функции в виде статистики. Пример использования в консоли:
python -m cProfile -o output.prof myscript.pyработа кода python (работа кода python)
После сбора данных статистику анализируют с помощью модуля pstats:
import pstatsp = pstats.Stats('output.prof')p.sort_stats('cumtime').print_stats(10)чтение кода python (чтение кода python)
В результате отображаются 10 функций с наибольшим суммарным временем (cumulative time).
Проблема: слишком много строк вывода; легко пропустить важное.
Решение: использовать сортировку по разным полям (time, calls) и ограничивать количество записей. Также можно отфильтровать только интересующий модуль.
Цель: быстрое выявление функций, занимающих больше всего времени.
Как посмотреть байт-код, в который преобразуется Python-программа?
Модуль dis дизассемблирует функцию или модуль в последовательность инструкций виртуальной машины. Пример:
import disdef add(a, b): return a + bdis.dis(add)
сравнение кода python (сравнение кода python)
Вывод показывает мнемоники байт-кода (LOAD_FAST, BINARY_ADD, RETURN_VALUE) с указанием строки исходного кода.
Проблема: начинающие разработчики игнорируют байт-код, считая его бесполезным.
Решение: использовать для понимания эффективности выражений - например, сравнить байт-код list comprehensions и генераторов.
Случаи использования: оптимизация горячих участков, написание быстрых алгоритмов, отладка неочевидного поведения.
Как точно измерить время выполнения маленького фрагмента кода?
Модуль timeit запускает код многократно и возвращает среднее время. Пример замера скорости двух вариантов:
import timeitcode1 = "sum(range(100))"code2 = "sum(list(range(100)))"t1 = timeit.timeit(code1, number=100000)t2 = timeit.timeit(code2, number=100000)print(f'{t1:.5f} vs {t2:.5f}')Проблема: результаты могут сильно различаться из-за шума ОС или сборки мусора.
Решение: запускать с большим числом повторений (number) и отключать сборщик мусора через setup.
Цель: объективное сравнение микрооптимизаций.
Как определить потребление памяти функциями?
Библиотека memory_profiler с декоратором @profile показывает прирост памяти на каждой строке. Установка: pip install memory_profiler. Пример использования в файле:
from memory_profiler import profile@profiledef create_list(): return [i for i in range(10000)]if __name__ == '__main__': create_list()Запуск: python -m memory_profiler script.py.
Проблема: профилирование памяти замедляет выполнение в 5-10 раз.
Решение: использовать только для отладки утечек, не в production.
Случаи использования: поиск мест, где создаются большие временные объекты.
Как добавить логирование времени выполнения без изменения структуры кода?
Декоратор, замеряющий время, можно написать самостоятельно:
import functools, timedef timer(func): @functools.wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) end = time.perf_counter() print(f'{func.__name__} занял {end-start:.4f} с') return result return wrapper@timerdef slow_function(): sum(range(10**6))slow_function()Проблема: декоратор не работает с корутинами (async/await).
Решение: написать отдельный декоратор для асинхронных функций, используя time.perf_counter() в асинхронном контексте.
Цель: мониторинг времени методов в процессе разработки.
Расширенные примеры анализа кода Python
Глубокий анализ с cProfile и pstats
Сохраним профиль в файл и извлечём топ-5 функций с наибольшим числом вызовов:
import cProfile, pstatsprofiler = cProfile.Profile()profiler.enable()# код, который нужно профилироватьsum(range(1000000))profiler.disable()stats = pstats.Stats(profiler)stats.sort_stats('ncalls').print_stats(5)ncalls tottime percall cumtime percall ... 1 0.042 0.042 0.042 0.042 ...
Для визуализации используйте snakeviz или gprof2dot.
Отслеживание утечек памяти с tracemalloc
Встроенный модуль tracemalloc позволяет увидеть, какие строки кода выделяют память:
import tracemalloctracemalloc.start()# выполнение кода с потенциальной утечкойdata = [bytearray(1000) for _ in range(1000)]snapshot = tracemalloc.take_snapshot()top_stats = snapshot.statistics('lineno')for stat in top_stats[:5]: print(stat)/path/to/script.py:5: size=1000 KiB, count=1000, average=1.0 KiB
Применение: выявление строк, где создаётся наиболее объёмная память.
Профилирование работающего процесса с py-spy
Инструмент py-spy не требует изменения кода и подключается к запущенному процессу:
py-spy top --pid 12345 # показывает текущую стек-трассуpy-spy record -o flamegraph.svg --pid 12345 # создаёт флейм-графикУстановка: pip install py-spy. Пример вывода (текстовый режим):
Thread 0x7f... (idle) main.worker main.process_request
Случай: профилирование production-серверов, где нельзя запускать cProfile.
Построчное профилирование с line_profiler
Модуль line_profiler (установка: pip install line_profiler) показывает время каждой строки:
@profiledef compute(): total = 0 for i in range(1000): total += i ** 2 return totalif __name__ == '__main__': compute()Запуск: kernprof -l -v script.py. Вывод (сокращён):
Line # Hits Time Per Hit % Time Line Contents============================================================== 2 @profile 3 def compute(): 4 1 2.0 2.0 0.5 total = 0 5 1000 1400.0 1.4 35.2 for i in range(1000): 6 1000 2558.0 2.6 64.3 total += i ** 2 7 1 0.0 0.0 0.0 return total
Ценность: видно, что строка 6 занимает почти 2/3 времени.