大数计算实战:Python decimal、gmpy2 与 NumPy 高精度运算

一、为什么 float 不够用

1
2
print(0.1 + 0.2 == 0.3)
print(0.1 + 0.2)

二进制浮点无法精确表示许多十进制小数。float 的有效精度约为 15~17 位,适合工程测量和机器学习中的近似计算,却不适合金额结算、密码学整数和高精度常数。

要先区分三类问题:

  • 定点金额:使用整数最可靠,例如以最小单位存储;
  • 十进制定点/科学计算:使用 decimal
  • 超大整数和高精度数学函数:使用 gmpy2mpmath
  • 向量化近似数值计算:使用 NumPy,但不能因此获得任意精度。

二、四大库对比

decimal 是标准库,支持上下文精度和银行家舍入,适合财务规则。gmpy2 基于 GMP、MPFR 和 MPC,整数和高精度浮点速度很快。mpmath 纯 Python 接口友好,适合任意精度数学函数。NumPy 的 float64 仍是双精度浮点,dtype=object 虽可放 Python 大整数,却失去向量化优势。

安装 gmpy2:

1
python -m pip install gmpy2

Linux 若缺少系统库,可先安装编译依赖;生产环境应固定 Python、编译器和 gmpy2 版本。

三、decimal:金额与定点计算

1
2
3
4
5
6
7
8
9
10
11
from decimal import Decimal, ROUND_HALF_UP, localcontext

price = Decimal("19.99")
quantity = Decimal("3")
tax = Decimal("0.13")

with localcontext() as ctx:
ctx.prec = 40
total = price * quantity * (Decimal("1") + tax)
total = total.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
print(total)

不要从 float 构造 Decimal:

1
2
Decimal(0.1)       # 带入 float 的二进制误差
Decimal("0.1") # 正确

getcontext().prec 控制有效数字,不是小数位数;固定小数位应使用 quantize()。在并发代码中可使用 localcontext(),避免全局上下文被其他模块改变。

四、gmpy2 与大整数

1
2
3
4
5
import gmpy2

n = gmpy2.mpz(2) ** 1024 - 1
print(n.bit_length())
print(gmpy2.is_prime(n))

RSA 素数生成示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import secrets
import gmpy2

def random_prime(bits: int) -> int:
while True:
candidate = secrets.randbits(bits)
candidate |= (1 << bits - 1) | 1
if gmpy2.is_prime(candidate, 25):
return candidate

p = random_prime(512)
q = random_prime(512)
n = p * q
print("p bits:", p.bit_length())
print("n bits:", n.bit_length())

这是演示代码,不是完整 RSA 密钥生成器。真实密码学应使用经过审计的库,如 cryptography,不能自行实现协议、填充和密钥格式。

五、PI 与链上金额

gmpy2 可以计算高精度 PI:

1
2
3
4
5
import gmpy2

gmpy2.get_context().precision = 3400
pi = gmpy2.const_pi()
print(gmpy2.mpfr(pi))

这里的 precision 使用二进制位。1000 位十进制约等于 3322 位二进制,设置 3400 可留出余量。

链上金额则优先整数化。若代币最小单位为 10^-18:

1
2
3
4
5
6
7
from decimal import Decimal

UNIT = 10**18
amount = Decimal("123.456789012345678901")
raw = int(amount * UNIT)
assert Decimal(raw) / UNIT == amount
print(raw)

乘法和除法时要明确舍入方向,不能直接把高精度 Decimal 转成 float。

六、复利计算与性能边界

1
2
3
4
5
6
7
8
9
10
from decimal import Decimal, localcontext

principal = Decimal("10000")
rate = Decimal("0.035")
years = 50

with localcontext() as ctx:
ctx.prec = 80
final = principal * (Decimal("1") + rate) ** years
print(final)

如果需求是金融结算,应按每期规则舍入,而不是只在最终结果舍入。两者可能产生不同结果。性能测试应固定输入、预热运行并统计多次,而不是凭一次执行时间判断“快 100 倍”。

七、踩坑记录

NumPy 的 float64 不能保存 256 位密码学整数。JSON 标准也没有统一的大整数语义,JavaScript 端可能将大整数转为不准确的 Number。接口传输时可以使用字符串,或拆成高低位并明确协议。

gmpy2mpzmpfr 不是所有 JSON 编码器都认识,序列化前显式转换为字符串。不要用 eval 反序列化大数。对用户输入设置位数上限,避免攻击者提交十亿位整数消耗 CPU 和内存。

八、小结

金额优先使用最小单位整数;需要十进制规则时使用 Decimal;高精度数学和超大整数优先 gmpy2;NumPy 只适合浮点近似和批量向量化。精度、舍入、序列化和性能必须作为同一个系统问题设计。


大数计算实战:Python decimal、gmpy2 与 NumPy 高精度运算
https://blog.calcguide.tech/2026-08-10-大数计算Python实战/
作者
王争气
发布于
2026年8月10日
许可协议