精确数 · 有理数、大整数与根式
符号计算的底气,是它的数从不四舍五入。1/3 就是 1/3,不是 0.333;50 的阶乘一位不差;√2 平方精确等于 2。这一章讲清 SymPy 的整数(任意精度)、有理数(精确分数)和根式,以及「精确」这件事到底意味着什么。
精确数 · 有理数、大整数与根式
上一章说表达式是树,叶子是符号与数。这一章专看数这类叶子——它是符号计算和数值计算分道扬镳的地方:符号计算里的数,永远精确,从不四舍五入。
有理数:1/3 就是 1/3
数值计算里, 只能存成 0.3333333333(近似),于是有了 0.1 + 0.2 != 0.3 那个经典的坑。符号计算根本不碰这个坑,因为它用精确的有理数:
from sympy import *
print(Rational(1, 3) + Rational(1, 6)) # 1/2 —— 精确!
print(Rational(1,10) + Rational(2,10) == Rational(3,10)) # TrueRational(1, 3) 是分子 1、分母 3 的精确分数,不是小数。分数相加时,SymPy 通分、约分,给出精确的 。数值计算里 0.1+0.2 那 的误差,在这里压根不会出现——因为这里没有浮点数。
小心 Python 的 /。 在 SymPy 里直接写 1/3 会先被 Python 算成浮点数 0.333…,精确性就丢了。要得到精确有理数,用 Rational(1,3),或者用 S(1)/3(S 把整数变成 SymPy 对象,于是 / 走符号除法):
print(1/3) # 0.3333333333333333 —— Python 浮点,已经不精确
print(S(1)/3) # 1/3 —— SymPy 有理数,精确这是新手最常见的一个坑:精确性从你敲下 / 的那一刻起就可能已经丢了。
大整数:要多大有多大
符号计算的整数是任意精度(bignum)的——不像普通编程语言的 int 有 64 位上限,它能表示要多大有多大的整数,一位都不差:
print(factorial(30)) # 265252859812191058636308480000000
print(Integer(2)**100) # 1267650600228229401496703205376 有 33 位, 有 31 位,普通 64 位整数早就溢出了,而符号计算精确无误地给出每一位。这是数论、密码学、组合计算的刚需——那里的数动辄几百上千位,差一位就全盘皆错。(Python 的内置 int 恰好也是任意精度的,这点上和 SymPy 一致;但多数语言的整数会溢出。)
根式与常数:符号地保留
最能体现「符号」二字的,是无理数。、、 这些没法用有限小数精确写出的数,符号计算干脆不算它们的小数,而是把符号原样保留,让它参与精确运算:
print(sqrt(8)) # 2*sqrt(2) —— 自动提取出完全平方因子
print(sqrt(2)**2) # 2 —— 精确,不是 2.0000000001
print(expand((sqrt(2)+1)*(sqrt(2)-1))) # 1 —— (√2+1)(√2-1)=2-1=1,精确
print(pi.is_irrational) # True 保留成符号,于是 精确等于 , 精确等于 。要是用数值算,,平方回去会得到 2.0000000000000004——又是舍入误差。符号计算把这些误差从源头消灭了。
需要小数时:要多少位给多少位
当然,最后你常常还是想要个小数(比如画图、报告结果)。符号计算能在你指定的任意精度下,把符号数转成小数——想要 40 位就给 40 位:
print(N(pi, 40)) # 3.141592653589793238462643383279502884197
print(N(sqrt(2), 50))注意这和浮点数的区别:浮点数固定 15~16 位有效数字,多了没有;而 N(pi, 1000) 能精确给出 的前 1000 位。精度由你说了算,这叫任意精度浮点。
精确,意味着什么
回头看,「精确数」给了符号计算三样数值计算给不了的东西:
| 数值(浮点) | 符号(精确) | |
|---|---|---|
0.333…(近似) | (精确) | |
| 溢出 / 近似 | 精确的 65 位整数 | |
2.0000…04 | 精确的 | |
| 误差 | 每步累积 | 完全没有 |
| 代价 | 快、省内存 | 慢、可能很占内存 |
没有误差,是符号计算能做代数的前提——你没法在「带随机误差的数」上做因式分解、判断两个式子是否恒等。当然,精确不是免费的:有理数要存分子分母、大整数要存每一位、根式越滚越长,又慢又占内存——这个代价会在表达式膨胀那章找上门来。
小结与下一章
符号计算的数永远精确:有理数( 精确,别被 Python 的 / 坑了,用 Rational 或 S(1)/3)、任意精度大整数( 一位不差)、根式与常数( 符号保留, 精确等于 2)。需要小数时用 N(expr, 位数) 要多少给多少。无误差,是做代数的前提。
树的结构有了、精确的数也有了,那 SymPy 会不会自动把式子化到最简?答案出人意料:只化一部分,而且必须如此。为什么 x+x 自动变 2x,sin²+cos² 却不自动变 1?下一章揭晓,这背后藏着一个深刻的不可能性。👉 自动化简的边界。
参考文献
- [1] Donald Knuth.《The Art of Computer Programming, Vol. 2: Seminumerical Algorithms》—— 任意精度算术与有理数。
- [2] SymPy 文档.「Gotchas: Rational, S(), N().」https://docs.sympy.org/latest/tutorials/intro-tutorial/gotchas.html
- [3] von zur Gathen & Gerhard.《Modern Computer Algebra》, ch. 5 —— 精确算术的实现。