Механизмы выполнения Python-программ

Раздел: 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 времени.

Работа кода Python - comments

En
работа кода python (python)