Мост к числам: subs, evalf, lambdify

SymPy вывел формулу — а считать-то нужно числа, и много. Мост между символьным и численным миром: subs, evalf и главный герой — lambdify.

lambdify — функция SymPy, превращающая символьное выражение в обычную быструю Python-функцию (или NumPy-функцию) для массовых численных вычислений.

Зачем нужен мост

Символьная математика прекрасна для вывода формул, но медленна для счёта. Когда формула получена (производная, интеграл, решение дифура), её нужно вычислить — часто в миллионах точек, чтобы построить график или прогнать симуляцию. Гонять для этого SymPy — самоубийство по скорости. Решение: один раз вывести формулу символьно, потом превратить её в быструю численную функцию. Это и есть стратегия всего курса — «руками/символьно поняли, численно посчитали».

Три инструмента, три задачи

ИнструментЧто делаетКогда
.subs(x, 2)подставляет значение, остаётся символьнымодна точка, нужна точность
.evalf(n)вычисляет с n знаками (хоть 50!)точное число с произвольной точностью
lambdifyпревращает в быструю функциюмассовый счёт (графики, симуляции)

evalf: произвольная точность

Уникальная способность SymPy — считать с любым числом знаков, далеко за пределы 17 цифр float (код для чтения):

import sympy as sp

print(sp.pi.evalf(50))    # 50 знаков числа pi!
# 3.1415926535897932384626433832795028841971693993751

print(sp.sqrt(2).evalf(30))   # 30 знаков корня из 2
print((sp.exp(1)).evalf(20))  # 20 знаков числа e

Обычный math.pi даёт лишь ~16 цифр; SymPy — сколько закажете. Это нужно в теории чисел и проверке гипотез, требующих сверхточности.

Идея lambdify «руками»

Что делает lambdify концептуально? Берёт формулу и строит из неё вычислимую функцию. Сэмулируем это на stdlib: «формула» как функция, которую мы массово применяем к точкам (как сделал бы lambdify для построения графика):

import math

# представим, что SymPy вывел формулу f(x) = x^2 * sin(x)
# lambdify превратил бы её вот в такую быструю функцию:
def f(x):
    return x**2 * math.sin(x)

# теперь массово считаем в 5 точках (как для графика)
xs = [0.0, 0.5, 1.0, 1.5, 2.0]
ys = [round(f(x), 4) for x in xs]

for x, y in zip(xs, ys):
    print(f"f({x}) = {y}")

Вывод:

f(0.0) = 0.0
f(0.5) = 0.1199
f(1.0) = 0.8415
f(1.5) = 2.2444
f(2.0) = 3.6372

Суть: формула один раз превращена в обычную функцию, и дальше счёт идёт на полной скорости Python/NumPy, без символьных накладных расходов.

lambdify в SymPy

import sympy as sp
import numpy as np

x = sp.symbols("x")
expr = x**2 * sp.sin(x)

# превращаем символьную формулу в NumPy-функцию
f = sp.lambdify(x, expr, "numpy")

xs = np.linspace(0, 2, 5)
print(f(xs))    # массив значений — посчитан numpy-скоростью!

# типичный конвейр: символьно вывели производную, численно посчитали
deriv = sp.diff(expr, x)
f_prime = sp.lambdify(x, deriv, "numpy")
print(f_prime(np.array([1.0, 2.0])))

Как работает под капотом

lambdify не интерпретирует выражение при каждом вызове — это было бы медленно. Вместо этого он генерирует исходный код Python-функции из дерева выражения и компилирует его через eval. Дерево x**2 * sin(x) превращается в строку "x**2 * sin(x)" с привязкой sin к numpy.sin, и эта строка становится настоящей функцией. Получается код, неотличимый по скорости от написанного руками. Именно поэтому конвейер «SymPy выводит формулу → lambdify компилирует → NumPy считает массивом» сочетает точность вывода с численной скоростью — лучшее из двух миров.

Частые ошибки

  • Использовать .subs() в цикле для скорости. Подстановка символьная и медленная; для массового счёта — только lambdify.
  • Забыть бэкенд "numpy". Без него lambdify создаст функцию на math (по одному числу), а не на массивах.
  • Путать evalf и float. evalf даёт точное число с заданной точностью; float() обрежет до 17 цифр double.

Итог

  • subs — подстановка в одной точке, evalf — число с произвольной точностью, lambdify — массовый счёт.
  • lambdify компилирует формулу в быструю Python/NumPy-функцию.
  • Главный конвейер курса: символьно вывели → lambdify → NumPy посчитал.
  • Так сочетают точность SymPy и скорость численного мира.
Проверьте себя
1. Зачем нужен lambdify?
AДля упрощения формул
BЧтобы превратить символьное выражение SymPy в быструю Python/NumPy-функцию для массового численного счёта
CДля решения уравнений
DДля печати LaTeX
2. Чем .evalf(50) отличается от обычного float?
AНичем
Bevalf вычисляет с заданным числом знаков (хоть 50), выходя далеко за пределы ~17 цифр double
Cevalf медленнее, но точность та же
Devalf округляет до целого
3. Как lambdify достигает скорости, близкой к рукописному коду?
AИнтерпретирует дерево при каждом вызове
BГенерирует исходный код функции из дерева выражения и компилирует его (через eval), привязывая функции к numpy/math
CИспользует GPU
DКэширует результаты