Мост к числам: 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 и скорость численного мира.