SymPy 快速上手
SymPy 用符号表示数学表达式,使运算可以保留精确结构,而不必立即求浮点近似值。是否精确也取决于输入:传入近似浮点数,并不能自动恢复原本的精确值。
from sympy import diff, integrate, simplify, solve, symbols
x = symbols("x", real=True)
expression = (x + 1) ** 2
expanded_derivative = diff(expression, x)
antiderivative = integrate(expression, x)
roots = solve(expression - 4, x)
identity = simplify((x**2 - 1) / (x - 1))
关键实践原则
- 显式定义符号:使用
symbols()创建变量。仅在逻辑上确实需要时添加假设(如real=True),不要随意添加,以免限制后续运算。 - 区分方程与表达式:当需要表示等式关系时,务必使用
Eq(left, right),而不是直接用==(后者在 Python 中是布尔比较)。 - 理解
solve()的返回结构:solve()返回的是结构化数据(通常是列表或字典),其具体形态取决于方程类型和求解目标。处理结果前,先检查数据结构。 - 精准化简优于通用化简:
- 如果你知道目标形式,优先使用特定函数:
factor()(因式分解)、expand()(展开)、cancel()(约分)。 simplify()是一个启发式(heuristic)函数,它尝试多种策略,但不保证得到最简或唯一的规范形式。在性能敏感或结果确定的场景下,避免盲目依赖simplify()。
- 如果你知道目标形式,优先使用特定函数:
建议先通读官方入门教程,掌握基础后,再针对高级假设(Assumptions)或特定求解器查阅对应模块文档。
精确输入与不可变表达式
本页范围是 SymPy 1.x 的标量符号代数、初等微积分与一元方程,假设读者理解 Python 表达式以及所用数学运算的含义。教程中的常见陷阱首先涉及输入:Python 会在 SymPy 接收前计算 1/3,因此精确分数应写成 Rational(1, 3)。Rational("0.1") 表示精确的十分之一;传入二进制浮点数无法事后恢复十进制原意。
from sympy import Eq, Rational, S, expand, lambdify, solveset, sqrt
third = Rational(1, 3)
assert 3 * third == 1
assert expanded_derivative == 2*x + 2
assert antiderivative == x**3/3 + x**2 + x
assert roots == [-3, 1]
assert identity == x + 1
value = expression.subs(x, third)
assert value == Rational(16, 9)
print(value.evalf(6)) # 1.77778
assert expression == (x + 1)**2
表达式不可变:subs、求导与变形返回新的表达式,不会修改 expression。integrate 返回一个原函数,不自动添加任意积分常数;写通用不定积分时仍需加常数。这里 integrate(expression, (x, 0, 1)) 的精确定积分为 7/3。需要近似数值时再用 evalf;增加显示位数不能修复已经被 Python 浮点舍入的输入。
相等、假设与定义域
== 检查结构相等,不是一般的数学等价。expand((x + 1)**2) == (x + 1)**2 为假,但两者之差可化简为零。构造方程用 Eq(若能直接判定相等与否,它也可能立即求值);把表达式传给求解器时,表示该表达式等于零。
因为 x 声明为实数,sqrt(x**2) 得到 Abs(x),不是 x。若 p = symbols("p", positive=True),则 sqrt(p**2) 可化简为 p。假设只允许在相应条件下成立的变换;无约束符号通常按复数考虑。
约分还可能掩盖定义域限制:(x**2 - 1)/(x - 1) 在 1 处无定义,化简后的 x + 1 却有定义。要把它们解释为同一个函数,必须保留原条件 x != 1。候选根应代回原方程检查,不能只检查变形后的分子。
real_roots = solveset(Eq(x**2, 2), x, domain=S.Reals)
assert real_roots == {-sqrt(2), sqrt(2)}
assert solveset(x**2 + 1, x, domain=S.Reals) == S.EmptySet
assert solveset(x**2 + 1, x, domain=S.Complexes) == {-S.ImaginaryUnit, S.ImaginaryUnit}
solveset 返回集合并接受显式定义域;不要以为符号的实数假设会改变它默认的复数求解域。未求出的 ConditionSet 表示求解器尚未确定解集,不表示无解。如果希望统一得到“符号到值”的字典列表,可用 solve(..., dict=True)。
转为数值计算
f = lambdify(x, expression, modules="math")
assert f(2) == 9
lambdify 把表达式转为数值函数。上面的 math 后端处理标量;对 NumPy 数组进行向量化求值可用 modules="numpy",但此时要遵守数值定义域与浮点规则。它会生成可执行代码,不能接收来自不可信输入的表达式。符号运算可能保留未求值形式,也可能代价很高;拿到符号对象不代表每个积分或方程都有闭式结果。