Перейти к основному содержимому

Оптимизация кода на Python

📺 Слайды к лекции

Материалы в разработке

Эта страница находится в процессе подготовки и не является финальной версией. Содержание будет дорабатываться и обновляться в течение текущего семестра.

Профилирование

Прежде чем оптимизировать, нужно найти узкие места. Python предоставляет несколько инструментов.

cProfile

cprofile.py
import cProfile

def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)

cProfile.run('fibonacci(30)')
python -m cProfile -s cumtime my_script.py

line_profiler

Построчное профилирование — показывает время каждой строки:

line_profiler.py
# pip install line_profiler
@profile
def slow_function():
total = 0
for i in range(1000000):
total += i * i
return total
kernprof -l -v my_script.py
Практический совет

Начинайте с cProfile для общей картины, затем используйте line_profiler для детального анализа горячих функций.

timeit — измерение времени

timeit.py
import timeit

# Сравнение подходов
t1 = timeit.timeit('sum(range(1000))', number=10000)
t2 = timeit.timeit(
'total = 0\nfor i in range(1000): total += i',
number=10000
)
print(f"sum(): {t1:.4f}s, for loop: {t2:.4f}s")

memory_profiler

memory.py
# pip install memory_profiler
from memory_profiler import profile

@profile
def create_large_list():
a = [i for i in range(1000000)]
b = [i**2 for i in a]
del a
return b
Частая ошибка

Преждевременная оптимизация — корень всех зол (Д. Кнут). Всегда профилируйте перед оптимизацией. 90% времени обычно тратится в 10% кода.

NumPy — векторизация

NumPy выполняет операции на уровне C, минуя интерпретатор Python:

numpy_opt.py
import numpy as np
import timeit

n = 1_000_000

# Чистый Python
def python_sum():
return sum(i * i for i in range(n))

# NumPy
arr = np.arange(n)
def numpy_sum():
return np.sum(arr * arr)

t1 = timeit.timeit(python_sum, number=10)
t2 = timeit.timeit(numpy_sum, number=10)
print(f"Python: {t1:.3f}s, NumPy: {t2:.3f}s")
# NumPy обычно в 10-100 раз быстрее
Практический совет

Избегайте циклов по элементам NumPy-массива. Вместо for x in arr: result += x*x используйте np.sum(arr * arr) — это работает на порядки быстрее.

Cython — компиляция Python в C

Cython — язык, расширяющий Python статическими типами. Компилируется в C:

fibonacci.pyx
# fibonacci.pyx — файл Cython
def fib_python(n):
if n < 2:
return n
return fib_python(n - 1) + fib_python(n - 2)

# С типизацией — в 10-100 раз быстрее
cpdef int fib_cython(int n):
if n < 2:
return n
return fib_cython(n - 1) + fib_cython(n - 2)
setup.py
from setuptools import setup
from Cython.Build import cythonize

setup(ext_modules=cythonize("fibonacci.pyx"))
python setup.py build_ext --inplace
Важно

Cython требует компилятора C (gcc/MSVC). Основное ускорение даёт объявление типов переменных (cdef int, cdef double) — без типов Cython работает не намного быстрее обычного Python.

Numba — JIT-компиляция

Numba компилирует Python-функции в машинный код на лету с помощью LLVM:

numba_example.py
from numba import jit, njit
import numpy as np

@njit # njit = jit(nopython=True)
def fast_sum(arr):
total = 0.0
for x in arr:
total += x * x
return total

arr = np.random.random(1_000_000)
result = fast_sum(arr) # Первый вызов — компиляция
result = fast_sum(arr) # Последующие — быстро

Режимы Numba

ДекораторРежимКогда использовать
@jitАвтоматическийПадает на object mode при ошибках
@njitnopython onlyГарантирует компиляцию, ошибка если не удаётся
@jit(parallel=True)ПараллельныйАвтоматическая параллелизация циклов
@jit(cache=True)С кэшемКэширует скомпилированный код на диск
Практический совет

Numba лучше всего работает с числовым кодом и NumPy-массивами. Не пытайтесь применять его к коду со строками, словарями или сложными объектами.

ctypes и cffi — вызов C-функций

ctypes_example.py
import ctypes

# Загрузка разделяемой библиотеки
lib = ctypes.CDLL("./libmath.so")

# Указание типов
lib.add.argtypes = [ctypes.c_int, ctypes.c_int]
lib.add.restype = ctypes.c_int

result = lib.add(3, 4)
print(result) # 7

Общие стратегии оптимизации

  1. Профилируйте перед оптимизацией
  2. Алгоритмическая оптимизация — самый большой выигрыш (O(n^2) -> O(n log n))
  3. Используйте встроенные функцииsum(), min(), max() реализованы на C
  4. Векторизация с NumPy — замена циклов на операции над массивами
  5. Numba/Cython — для числовых вычислений
  6. multiprocessing — для CPU-bound задач, обходящий GIL
  7. Кэшированиеfunctools.lru_cache, Redis
Определение

Правило 80/20: 80% времени выполнения приходится на 20% кода. Оптимизируйте только критические участки, найденные профилированием.