Символы и выражения: строим формулы
Прежде чем дифференцировать и решать, надо научиться создавать символы и собирать из них выражения — это азбука SymPy.
Символ (Symbol) — математическая переменная (например, x), с которой SymPy работает буквенно; из символов и операций строятся выражения.
Создание символов
Главная функция — symbols(). Она создаёт один или несколько символов сразу:
import sympy as sp
x = sp.symbols("x") # один символ
a, b, c = sp.symbols("a b c") # три сразу
i, j = sp.symbols("i j", integer=True) # с предположением: целые
expr = a * x**2 + b * x + c # обычное квадратное выражение
print(expr) # a*x**2 + b*x + c
Важный момент: имя переменной Python и имя символа — это разные вещи. x = symbols("igrek") создаст символ, который печатается как «igrek», но в коде вы зовёте его x. По привычке их делают одинаковыми, чтобы не путаться.
Предположения (assumptions)
SymPy можно подсказать свойства символа — это влияет на упрощения. Например, positive=True позволит упростить sqrt(x²) до x (без модуля):
| Предположение | Что задаёт |
positive=True | символ строго больше нуля |
real=True | вещественное число |
integer=True | целое число |
nonzero=True | не равно нулю (можно делить) |
Подстановка значений
Метод .subs() заменяет символ на число или другое выражение — это мост от формулы к конкретному значению:
import sympy as sp
x = sp.symbols("x")
expr = x**2 + 2*x + 1
print(expr.subs(x, 3)) # 16 — подставили x=3, получили точно
print(expr.subs(x, sp.Rational(1, 2))) # 9/4 — точная дробь
«Дерево выражения» руками на stdlib
Чтобы понять, что SymPy хранит выражения деревом, построим крошечный «символьный калькулятор» на чистом Python: представим выражение вложенными кортежами и вычислим его, подставив значение x:
# Выражение x^2 + 2*x + 1 как дерево из кортежей
# ('+', левое, правое), ('*', ...), ('^', ...), ('x',), число
expr = ('+',
('+', ('^', ('x',), 2),
('*', 2, ('x',))),
1)
def evaluate(node, x):
if isinstance(node, (int, float)):
return node
op = node[0]
if op == 'x':
return x
if op == '+':
return evaluate(node[1], x) + evaluate(node[2], x)
if op == '*':
return evaluate(node[1], x) * evaluate(node[2], x)
if op == '^':
return evaluate(node[1], x) ** node[2]
print("При x=3 :", evaluate(expr, 3)) # 9 + 6 + 1 = 16
print("При x=0 :", evaluate(expr, 0)) # 0 + 0 + 1 = 1
Вывод:
При x=3 : 16 При x=0 : 1
Это ровно то, что .subs() делает внутри SymPy: обходит дерево и подставляет значение в листья-символы. Только настоящий SymPy умеет ещё и не вычислять, а преобразовывать такое дерево.
Как работает под капотом
Когда вы пишете a*x**2 + b*x + c, Python вызывает перегруженные операторы SymPy-объектов: ** создаёт узел Pow, * — Mul, + — Add. Результат — иммутабельное (неизменяемое) дерево: SymPy никогда не меняет выражение на месте, а строит новое. Это делает выражения безопасными для повторного использования и кэширования. Ещё SymPy автоматически делает мелкие упрощения при построении (например, x + x сразу станет 2*x), но крупные оставляет вам через simplify.
Частые ошибки
- Забыть создать символ. Использование
xбезx = symbols("x")даст обычную Python-ошибку «имя не определено». - Думать, что
.subs()меняет выражение. Оно возвращает новое выражение; старое не трогается (иммутабельность). - Игнорировать предположения. Без
positive=TrueSymPy не упроститsqrt(x**2)— он не знает, что x ≥ 0.
Итог
- Символы создаются через
symbols(); из них и операций строятся выражения. - Предположения (
positive,real,integer) управляют упрощениями. .subs()подставляет значение и возвращает новое (точное) выражение.- Под капотом — иммутабельные деревья;
.subs()— это обход дерева.