2. 符号表达式核心操作

在 SymPy 中,一切符号推导都建立在“符号对象”之上。符号(Symbol)是表达式的最小原子,表达式(Expr)是由符号、数字和运算符递归组合而成的树形结构。理解这一层结构,才能理解为什么 simplify 有时“化简不动”、为什么 subs 有时“替换不进去”、为什么 expand 与 factor 互为逆操作。本节围绕符号创建、表达式构建、化简与展开、替换与模式匹配、方程与不等式表示、表达式树遍历六个核心操作展开,并给出可直接运行的示例。

一、符号创建与表达式构建

创建符号的标准方式是 symbols(),它支持一次生成多个符号、指定假设条件(assumptions)。假设条件不是装饰,而是会直接影响化简与判断结果:声明 positive=True 后,sqrt(x**2) 才能化简为 x;声明 integer=True 后,sin(n*pi) 才能自动化为 0。

from sympy import symbols, sqrt, sin, pi, Rational

x, y = symbols('x y')                 # 无假设
a, b = symbols('a b', positive=True)  # 正实数假设
n = symbols('n', integer=True)        # 整数假设

print(sqrt(a**2))      # a
print(sqrt(x**2))      # sqrt(x**2),未化简
print(sin(n*pi))       # 0
print(Rational(1, 3) + Rational(1, 6))  # 1/2,精确有理数

表达式构建有两种等价路径:一是直接用 Python 运算符拼接,如 expr = (x + 1)**2 - 3*x;二是用 sympify() 或 parse_expr() 从字符串解析,适合从配置文件或用户输入构造表达式。注意 sympify("x^2") 会把 ^ 解释为异或,应写成 x**2 或使用 parse_expr("x^2", transformations=standard_transformations)。构建完成后,expr.args 返回直接子节点,expr.free_symbols 返回全部自由符号,这是后续遍历的入口。

二、化简与展开:方向相反的两类操作

化简与展开不是“谁更高级”的关系,而是沿表达式树向不同方向重组:expand 把乘积、幂、三角函数和式拆开,使树变“宽”;factor、together、cancel、simplify 则把树收“窄”。选择哪一个,取决于你下一步要做什么——求导、积分、解方程还是数值代入。

操作典型输入结果适用场景
expand(x+1)**2x**2 + 2*x + 1逐项求导、系数提取
factorx**2 - 1(x-1)*(x+1)找零点、约分
together1/x + 1/y(x+y)/(x*y)通分后比较分子
cancel(x**2-1)/(x-1)x + 1有理函数约分
simplifysin(x)**2+cos(x)**21通用试探,代价较高

实践建议:优先使用“目标明确”的函数(factor、cancel、trigsimp),把 simplify 作为兜底。因为 simplify 内部会尝试多种策略并比较结果,在大表达式上可能耗时数秒甚至更久。若需控制代价,可用 simplify(expr, ratio=1.7) 限制尝试强度,或用 count_ops(expr) 量化复杂度,比较化简前后操作数是否真的减少。

三、替换与模式匹配

subs 是最常用的替换:expr.subs(x, 2) 做精确替换,expr.subs({x: 2, y: 3}) 做批量替换。它按表达式树自底向上匹配,默认只替换“完全相等”的子表达式,因此 (x+1).subs(x+1, t) 能成功,而 (x+1).subs(x, t) 得到 t+1。若要做结构化替换,应使用 replace() 配合 Wild 通配符:

from sympy import symbols, Wild, sin, cos

x = symbols('x')
w = Wild('w')
expr = sin(x)**2 + sin(x)*cos(x)

# 把所有 sin(...) 替换为 u
print(expr.replace(sin(w), w))        # x**2 + x*cos(x)

# 只替换 sin(x)**2 这一模式
print(expr.replace(sin(w)**2, w))     # x + sin(x)*cos(x)

注意 replace 的匹配是“自顶向下尝试、命中即停”,与 subs 的精确相等语义不同。做符号推导时,若发现替换“没生效”,先检查目标子表达式是否与模式在结构上完全一致(包括幂次、系数、函数名),再用 expr.atoms() 或 srepr(expr) 查看真实结构。

四、方程与不等式表示

SymPy 中方程用 Eq(lhs, rhs) 表示,不等式用 Lt、Le、Gt、Ge 表示。它们本身是布尔表达式,可参与 solve、solveset、reduce_inequalities 等求解函数。注意 Eq(x, 1) 与 Python 的 x == 1 不同:后者在 SymPy 中会返回 False(因为比较的是对象身份),必须显式使用 Eq。

from sympy import symbols, Eq, Le, solve, solveset, S, reduce_inequalities

x = symbols('x', real=True)
eq = Eq(x**2 - 5*x + 6, 0)
print(solve(eq, x))                    # [2, 3]
print(solveset(eq, x, domain=S.Reals)) # {2, 3}

ineq = Le(x**2 - 4, 0)
print(reduce_inequalities(ineq, x))    # (-2 <= x) & (x <= 2)

选择建议:solve 返回列表,适合有限解集;solveset 返回集合,能表达区间、无穷解集和空集,语义更严谨;不等式优先用 reduce_inequalities,它会把复合条件整理为合取式。

五、表达式树遍历与操作

SymPy 表达式本质是 Basic 子类构成的树,每个节点有 func(节点类型)与 args(子节点元组)。遍历方式有三种:preorder_traversal 前序、postorder_traversal 后序、以及递归手写。前序遍历适合“查找并替换”,后序遍历适合“自底向上求值”。

表达式树:(x + 1)**2 − 3*x 节点标注:func 类型 / 前序序号 Add ① 加法节点 Pow ② 幂运算 Mul ⑥ 乘法节点 Add ③ x + 1 Integer ⑤ 2 Symbol ④ Integer ④ Integer ⑦ -3 Symbol ⑧ x Add Pow Mul 原子

上图给出 expr = (x + 1)**2 - 3*x 的树结构与前序遍历序号。前序遍历顺序为 Add → Pow → Add → Symbol(x) → Integer(1) → Integer(2) → Mul → Integer(-3) → Symbol(x),共 9 个节点。用代码验证:

from sympy import symbols, preorder_traversal, postorder_traversal, count_ops

x = symbols('x')
expr = (x + 1)**2 - 3*x

nodes = list(preorder_traversal(expr))
print(len(nodes))                       # 9
print([type(n).__name__ for n in nodes])
# ['Add', 'Pow', 'Add', 'Symbol', 'Integer', 'Integer', 'Mul', 'Integer', 'Symbol']

print(count_ops(expr))                  # 5,衡量表达式复杂度
print(expr.subs(x, 2))                  # 3,代入验证

遍历的实际用途有三类:其一,统计某类子表达式出现次数,例如统计 sin 节点数量以判断三角化简是否彻底;其二,批量改写,例如把所有 Integer 常量替换为浮点近似;其三,配合 count_ops 做化简效果评估。需要提醒的是,SymPy 会自动做规范化(如 Add 的子节点按固定顺序排列、Mul 把数值系数提到最前),因此手写递归时不要假设子节点顺序与书写顺序一致,应以 func 和 args 为准。

实践闭环建议:拿到一个待推导表达式后,按“创建符号并声明假设 → 构建表达式 → 用 count_ops 记录初始复杂度 → 选择目标明确的化简/展开函数 → 用 subs 代入若干组数值做数值验证 → 比较化简前后在相同代入点的数值是否一致”的顺序推进。数值验证是符号推导的必要补充:若化简前后在多个随机点上数值不一致,说明化简方向或假设条件有误,应回退检查。

最后强调一个常见陷阱:simplify 的结果依赖假设条件与版本实现,不同 SymPy 版本可能给出形式上不同但数学等价的结果。因此工程中不要把“化简后的字符串”作为断言依据,而应使用 simplify(expr1 - expr2) == 0 或 expr1.equals(expr2) 做等价性判断,前者对多数初等表达式可靠,后者会做数值抽样验证,二者结合使用更稳妥。