精确数 · 有理数、大整数与根式

符号计算的底气,是它的数从不四舍五入。1/3 就是 1/3,不是 0.333;50 的阶乘一位不差;√2 平方精确等于 2。这一章讲清 SymPy 的整数(任意精度)、有理数(精确分数)和根式,以及「精确」这件事到底意味着什么。

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

精确数 · 有理数、大整数与根式

上一章说表达式是树,叶子是符号与数。这一章专看数这类叶子——它是符号计算和数值计算分道扬镳的地方:符号计算里的数,永远精确,从不四舍五入。

有理数:1/3 就是 1/3

数值计算里,13\frac13 只能存成 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))   # True

Rational(1, 3) 是分子 1、分母 3 的精确分数,不是小数。分数相加时,SymPy 通分、约分,给出精确的 12\frac12。数值计算里 0.1+0.2 那 10−1710^{-17} 的误差,在这里压根不会出现——因为这里没有浮点数。

小心 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

30!30! 有 33 位,21002^{100} 有 31 位,普通 64 位整数早就溢出了,而符号计算精确无误地给出每一位。这是数论、密码学、组合计算的刚需——那里的数动辄几百上千位,差一位就全盘皆错。(Python 的内置 int 恰好也是任意精度的,这点上和 SymPy 一致;但多数语言的整数会溢出。)

根式与常数:符号地保留

最能体现「符号」二字的,是无理数。2\sqrt2、π\pi、ee 这些没法用有限小数精确写出的数,符号计算干脆不算它们的小数,而是把符号原样保留,让它参与精确运算:

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\sqrt2 保留成符号,于是 (2)2(\sqrt2)^2 精确等于 22,(2+1)(2−1)(\sqrt2+1)(\sqrt2-1) 精确等于 11。要是用数值算,2≈1.41421356\sqrt2\approx1.41421356,平方回去会得到 2.0000000000000004——又是舍入误差。符号计算把这些误差从源头消灭了。

需要小数时:要多少位给多少位

当然,最后你常常还是想要个小数(比如画图、报告结果)。符号计算能在你指定的任意精度下,把符号数转成小数——想要 40 位就给 40 位:

print(N(pi, 40))     # 3.141592653589793238462643383279502884197
print(N(sqrt(2), 50))

注意这和浮点数的区别:浮点数固定 15~16 位有效数字,多了没有;而 N(pi, 1000) 能精确给出 π\pi 的前 1000 位。精度由你说了算,这叫任意精度浮点。

精确,意味着什么

回头看,「精确数」给了符号计算三样数值计算给不了的东西:

数值(浮点)符号(精确)
13\frac130.333…(近似)13\frac13(精确)
50!50!溢出 / 近似精确的 65 位整数
22\sqrt2^22.0000…04精确的 22
误差每步累积完全没有
代价快、省内存慢、可能很占内存

没有误差,是符号计算能做代数的前提——你没法在「带随机误差的数」上做因式分解、判断两个式子是否恒等。当然,精确不是免费的:有理数要存分子分母、大整数要存每一位、根式越滚越长,又慢又占内存——这个代价会在表达式膨胀那章找上门来。

小结与下一章

符号计算的数永远精确:有理数(13\frac13 精确,别被 Python 的 / 坑了,用 Rational 或 S(1)/3)、任意精度大整数(50!50! 一位不差)、根式与常数(2,π\sqrt2,\pi 符号保留,22\sqrt2^2 精确等于 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 —— 精确算术的实现。

本页目录