表达式即树 · 符号计算的第一性原理

你眼里的公式 x²+2x+1 是一行字符,计算机眼里却是一棵结构化的树:一个加法节点,挂着三个孩子。这一章用 SymPy 的 srepr、.func、.args 把这棵树剖开——理解了「表达式是树」,你就抓住了整个符号计算的地基。

作者 David更新于 第 1 篇(共 18 篇)

表达式即树 · 符号计算的第一性原理

符号计算的一切,都建立在一个朴素却深刻的洞察上。这一章只讲这一个洞察,但请务必吃透它——后面每一章都站在它上面。

公式在计算机眼里,是一棵树

你写下 x2+2x+1x^2 + 2x + 1,看到的是一行数学符号。但计算机不能把它当字符串处理(那样连「加法有结合律」都不知道)。它把这个式子理解成一棵树:

图表加载中…

最顶上是一个加法节点,它有三个孩子:x2x^2(一个幂节点)、2x2x(一个乘法节点)、常数 11。幂节点又有两个孩子 xx 和 22……一直到叶子——符号(xx)和数字(1,21,2)——为止。这棵树,就是这个公式在计算机里的真身。

用 SymPy 把树剖开

空口无凭。SymPy 的 srepr(structural repr,结构化表示)能把这棵树原样打印出来:

from sympy import *
x = symbols('x')
e = x**2 + 2*x + 1

print(srepr(e))
# Add(Pow(Symbol('x'), Integer(2)), Mul(Integer(2), Symbol('x')), Integer(1))

看清楚了吗?Add(Pow(...), Mul(...), Integer(1))——这正是上面那棵树的文字版:一个 Add,套着一个 Pow、一个 Mul 和一个 Integer(1)。你平时看到的漂亮公式 x**2 + 2*x + 1,只是这棵树的「显示皮肤」;它的真身是 Add(Pow(...), ...)。

每个表达式 = 函数 + 参数

这棵树有一个极其规整的结构,规整到可以用一句话概括:

在 SymPy 里,每个表达式都是 函数(参数...) 的形式——.func 给出根节点是什么运算,.args 给出它的孩子。 这个规律递归地贯穿整棵树,直到叶子:

print(e.func)     # <class 'sympy.core.add.Add'>   根是「加法」
print(e.args)     # (1, x**2, 2*x)                  三个孩子

# 递归下去:取第三个孩子 2*x,它又是 函数+参数
child = e.args[2]           # 2*x
print(child.func)           # <class 'sympy.core.mul.Mul'>
print(child.args)           # (2, x)

这个 「.func + .args」的统一结构,是符号计算所有算法的立足点:想遍历、想改写一个表达式,你只需递归地处理 func 和每个 arg。一个几行的递归函数,就能走遍任意复杂公式的每个角落。

顺带一提,e.args 是 (1, x**2, 2*x) 而不是你输入的顺序——因为加法可交换,SymPy 会把参数排成一个规范顺序,这样 x2+2x+1x^2+2x+1 和 1+2x+x21+2x+x^2 会得到同一棵树。这个「规范化」的思想,下一章还会细讲。

叶子:符号与数

一棵树总会到头。表达式树的叶子(不能再拆的原子)只有两类:符号(如 xx,代表未知量)和数(如 1,21,2)。SymPy 的 .atoms() 能把它们捞出来:

print(e.atoms(Symbol))    # {x}      —— 所有符号
print(e.atoms(Number))    # {1, 2}   —— 所有数字

内部节点是运算(加、乘、幂、sin、积分符号……),叶子是符号和数。任何数学表达式,无论多复杂,都是这样一棵「运算当枝、符号与数当叶」的树。

为什么这件事如此重要

因为符号计算做的一切,本质都是在操作这棵树:

  • 求导 = 按求导规则,把树递归地变成另一棵树(和的导数 = 导数的和,乘积法则……)。
  • 化简 = 在树里找到可以变简单的部分,替换掉。
  • 代入求值 = 在树里找到某个符号,把它换成一个值。
  • 解方程 = 把方程的树,变形成 x = 某个表达式 的树。

后面所有章节,拆开看都是「对树做变换」。所以请记住这句话——它是本专栏的第一性原理:

公式是树;符号计算,就是操作这棵树。

小结与下一章

任何数学表达式在计算机里都是一棵树:内部节点是运算、叶子是符号与数。SymPy 里每个表达式都遵循 .func(是什么运算)+ .args(孩子们) 的统一结构,递归到底;srepr 能打印真身,.atoms() 能捞出叶子。符号计算的一切,都是对这棵树的操作。

树是「形」,但树的叶子——数——本身也大有讲究:符号计算里的数是精确的(有理数、大整数、根式),永不四舍五入。下一章看这些精确数,以及它们为什么是符号计算的底气。👉 精确数:有理数、大整数与根式。

参考文献

本页目录