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

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

- 作者：David（道雾轩）
- 专栏：符号计算的艺术（https://daiw.org/manual/symbolic-computing.md）
- 最后更新：2026-07-14
- 原文：https://daiw.org/manual/symbolic-computing/expression-tree
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

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

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

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

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

```mermaid
flowchart TD
    A["Add · 加法"] --> B["Pow · 幂"]
    A --> C["Mul · 乘法"]
    A --> D["Integer · 1"]
    B --> E["Symbol · x"]
    B --> F["Integer · 2"]
    C --> G["Integer · 2"]
    C --> H["Symbol · x"]
```

最顶上是一个**加法**节点，它有三个孩子：$x^2$（一个**幂**节点）、$2x$（一个**乘法**节点）、常数 $1$。幂节点又有两个孩子 $x$ 和 $2$……一直到叶子——**符号**（$x$）和**数字**（$1,2$）——为止。这棵树，就是这个公式在计算机里的真身。

## 用 SymPy 把树剖开

空口无凭。SymPy 的 `srepr`（structural repr，结构化表示）能把这棵树**原样打印**出来：

```python
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(...), ...)`。**

## 每个表达式 = 函数 + 参数

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

<Callout type="info">
  **在 SymPy 里，每个表达式都是 `函数(参数...)` 的形式——`.func` 给出根节点是什么运算，`.args` 给出它的孩子。** 这个规律**递归地**贯穿整棵树，直到叶子：

  ```python
  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`。一个几行的递归函数，就能走遍任意复杂公式的每个角落。
</Callout>

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

## 叶子：符号与数

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

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

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

## 为什么这件事如此重要

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

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

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

> **公式是树；符号计算，就是操作这棵树。**

## 小结与下一章

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

树是「形」，但树的叶子——**数**——本身也大有讲究：符号计算里的数是**精确**的（有理数、大整数、根式），永不四舍五入。下一章看这些精确数，以及它们为什么是符号计算的底气。👉 [精确数：有理数、大整数与根式](https://daiw.org/manual/symbolic-computing/exact-numbers)。

## 参考文献

- [1] Joel S. Cohen.《Computer Algebra and Symbolic Computation: Elementary Algorithms》—— 表达式树与递归结构。
- [2] SymPy 文档.「Tutorial: Manipulation — srepr, args, func.」https://docs.sympy.org/latest/tutorials/intro-tutorial/manipulation.html
- [3] Cohen.《...: Mathematical Methods》—— 表达式的完全递归定义。
