几何级数这个词听着像课本里压在最后一章的老古董可只要写过一行处理衰减系数的代码、算过一次无限期的现金流、或者在代码评审里被人问过你这个循环到底会不会收敛其实就已经在跟它打交道了。它的定义朴素得不像话每一项都是前一项乘上同一个固定的数写成式子就是 a ar ar² ar³ …。但就是这么个简单结构撑起了收敛条件这套判定逻辑也撑起了大量工程里无限求和的落地方法。这篇文章不讲考试题只讲三件事几何级数为什么在 |r| 1 时收敛、边界上那几个特殊值到底发生了什么、以及怎么用 Python 把它算准而不是算出一个似是而非的数。刚接触级数的读者能拿到完整的直觉和可复制的代码做过数值计算的读者能在这里找到那几个容易翻车的浮点细节。1. 几何级数到底在算什么定义、三层直觉与部分和1.1 一个固定倍数带来的结构几何级数也叫等比级数的标准形式是 Σ_{k0}^{∞} a·r^k其中 a 是首项r 是公比。这里有个坑必须先说清楚很多教材从 k1 开始写形式变成 Σ a·r^{k-1}两者只差一个下标位移但你在套公式的时候一旦搞混首项到底算第 0 项还是第 1 项结果就会整体差一个 r 倍。我自己习惯一律从 k0 写起这样第 n 项就是 a·r^n无限和是 a/(1-r)有限和是 a(1-r^{n1})/(1-r)首项和指数能对上不容易错。为什么这个结构值得单独拎出来讲因为绝大多数能手算出来的无限求和最后都归约到它。泰勒展开在某个点取值、概率里的几何分布、信号处理里的指数衰减、金融里的年金现值表面看各不相干落到式子上都是同一个形状每次乘一个固定的比例。抓住这个共性你就不需要记十来个公式只需要记一个。还有一个容易忽略的前提讨论收敛性时默认 a ≠ 0。a 0 时所有项都是 0和恒为 0收敛性当然成立但这个平凡情形没有任何信息量教材里通常直接跳过。写代码时倒是要单独处理因为 0·r^k 在 r 0 且 k 0 时会碰到 0^0 的约定问题Python 里 0**0 返回 1正好和首项是 a的约定一致。1.2 部分和公式是怎么推出来的有限项的和记作 S_n a ar ar² … ar^n一共 n1 项。推导只用一招错位相减。把 S_n 乘上 r得到 rS_n ar ar² … ar^{n1}两个式子上下对齐相减中间所有项两两抵消只剩头尾(1-r)S_n a - a·r^{n1} a(1 - r^{n1})。于是当 r ≠ 1 时S_n a(1 - r^{n1})/(1 - r)当 r 1 时这个式子分母为零失效得回到原始定义每一项都等于 a共 n1 项S_n (n1)a。这一步看起来是废话但它是后面代码里必须单独开分支的原因我见过太多人直接在代码里写 a*(1-r**n)/(1-r) 然后在 r1 时收获一个 ZeroDivisionError。错位相减这个技巧的适用范围远不止几何级数。等差乘等比形成的级数比如 Σ k·r^k、某些差分方程的通解、生成函数的化简用的都是同一套动作乘一个因子、错位、相减、消掉中间项。所以这段推导别当成一次性公式背下来它是你后面处理更复杂求和的基本功。1.3 三种直觉几何折叠、误差传递、Zeno 悖论第一种直觉是几何的。把总和看成一条长度为 1 的线段先取 r 倍长度再从剩下的里面取 r 倍如此反复。当 0 r 1 时每一轮都只拿剩余部分的一个固定比例剩余量按 (1-r) 的比例指数缩小永远不会取完但取走的长度总和是有限的。这就是 Zeno 悖论的正解走一半、再走剩下的一半、再走剩下的一半无限步之后总路程恰好是 1无限步不等于无限远。第二种直觉是误差传递。设当前的部分和与真实和的差为 d_n那么再补一项之后误差变成 d_{n1} r·d_n 尾部的残余主导项就是乘上 r。所以误差每一轮都被压缩到原来的 r 倍压缩因子小于 1误差必然趋于零。这个视角比公式取极限更贴近工程你在迭代算法里看到的收敛速度几乎都能表述成误差每轮乘一个小于 1 的因子。第三种直觉来自离散动力系统。把部分和序列 S_0, S_1, S_2, … 看成一个迭代过程它满足 S_{n1} r·S_n a这是一个一阶线性递推。它的不动点解 S* a/(1-r)收敛到不动点的条件正是 |r| 1。换句话说几何级数收敛这件事和线性迭代能不能稳定下来是同一件事的两种说法。理解了这一层你看到马尔可夫链的幂迭代、滤波器冲激响应、折现因子衰减都会觉得眼熟。2. 收敛条件不是一句 |r| 1 的口诀2.1 用部分和公式把收敛性彻底说透判断收敛本质上就是判断 lim_{n→∞} S_n 是否存在。把 S_n a(1 - r^{n1})/(1-r) 拆开看a 和 (1-r) 都是常数唯一会随 n 变化的是 r^{n1}。所以问题被压缩成一句话r^{n1} 在 n 趋于无穷时有没有极限。当 |r| 1 时r^n → 0于是 S_n → a/(1-r)收敛。当 r 1 时S_n (n1)a随 n 线性增长发散到无穷a 0 时。当 r -1 时S_n 在 a 和 0 之间来回跳没有极限属于有界但发散。当 |r| 1 时|r|^n → ∞S_n 的绝对值无界发散。|r| 1 时 r^n → 0 这个结论需要一个像样的证明不然就只是课本这么说。思路是把 1/|r| 写成 1 h 的形式其中 h 0。由二项式展开或伯努利不等式(1h)^n ≥ 1 nh nh取倒数得 |r|^n 1/(1h)^n 1/(nh)。右边趋于 0夹逼一下就得到 |r|^n → 0。这个证明只用到 h 0 这一个条件而 h 0 恰好等价于 |r| 1所以收敛的充要条件就锁死了。注意这里的收敛指的是部分和序列有有限极限。r -1 的情形常被误判因为它的部分和有界、上下震荡看起来没跑远但极限不存在就是不收敛。判断标准是极限存在与否不是有界与否。2.2 边界上的几个特殊值以及复数情形r 1 是最容易忽视的边界。它的有限和是 (n1)a无限和发散但因为形式简单很多人在写代码时直接用通用公式结果除以零。正确做法是在函数入口就判断 r 1走单独分支。r -1 更有迷惑性。部分和序列是 a, 0, a, 0, a, 0, …两个子列分别收敛到 a 和 0整体不收敛。这类子列极限不同的振荡发散在数值实验里也很难靠多算几项发现因为你看到的数始终在 a 附近只是永远定不下来。判断依据仍然是数学上的极限存在性不是数值上的看起来稳定。|r| 1 但 r 不是实数比如 r i情形也一样r^n 在 i, -1, -i, 1 之间循环部分和有界但无极限。所以完整的收敛条件是 |r| 1这里的绝对值是复数的模不是实数的绝对值。写代码时用 abs(r) 就对Python 的 abs 对 complex 返回模长这点很省事。|r| 1 时也不是全无用处。有限项和公式依然成立二叉树节点计数 1 2 4 … 2^h 2^{h1} - 1 就是 r 2 的有限和。区别只在于有限和随便算无限和必须 |r| 1。这个区分在工程里很关键比如容量规划算的是有限和而稳态误差分析要求无限和收敛。|r| 1 时无限和等于 a/(1-r)这个式子在 r 为复数时同样成立结果是复数。比如 a 1, r i/2和就是 1/(1 - i/2) (1 i/2)/(1.25) 0.8 0.4i。这种复数几何级数在信号处理里很常见Z 变换的收敛域讨论本质上就是在讨论这个。2.3 收敛速度、截断误差与项数估算知道收敛还不够工程上真正关心的是要算多少项才够。设 |r| 1截断误差满足 |S - S_n| |a|·|r|^{n1}/(1-|r|)。这个式子可以直接反解出所需项数要求误差不超过 ε则|r|^{n1} ≤ ε(1-|r|)/|a|两边取对数注意 ln|r| 0除的时候不等号翻转n 1 ≥ ln(ε(1-|r|)/|a|) / ln|r|取上整再减一就是最小项数。取 a 1、ε 1e-6不同 r 的结果差异极大公比 r达到 1e-6 精度所需项数 n说明0.520收敛很快日常够用0.9152仍然可控0.991832开始变慢0.99920712明显吃力-0.520负公比不影响误差界-0.9152误差界由模长决定这张表值得盯几秒。r 从 0.9 挪到 0.99只变了 0.09项数涨了十二倍再挪到 0.999又涨了十倍多。这就是收敛和快速收敛的区别也是很多迭代算法明明理论收敛、实际跑不动的原因。模长越接近 1收敛越慢这条规律在矩阵情形下同样成立。提示这个误差界是上界不是精确值。对 r 0 基本贴紧对 r 0 时因为正负项交替实际误差通常比界小很多。所以估算项数时可以放心用这个界它是偏保守的。还有一个隐藏前提这个界只描述数学上的截断误差不包含浮点运算本身的舍入误差。当 r 非常接近 1 时项数会大到让浮点误差开始主导那时候算出来的精度根本达不到理论值。这个坑在第 3 节详细说。3. Python 实战符号推导、数值验证与浮点陷阱3.1 先用符号计算把公式坐实动手写数值代码之前先用 sympy 做一遍符号推导好处是能把分支条件看清楚。下面这段代码求有限部分和的闭式表达from sympy import symbols, summation, oo, simplify, Rational, Sum k, n symbols(k n, integerTrue, nonnegativeTrue) a symbols(a, nonzeroTrue) r symbols(r) S_n summation(a * r**k, (k, 0, n)) print(S_n)我这边的环境跑出来的结果是一个分段表达式主分支是 a*(1 - r**(n 1))/(1 - r)附加条件是 r ≠ 1另一个分支是 a*(n 1)对应 r 1 的情形。这个输出恰好把第 1 节手推的结论原样复现了一遍等于是一次自动校验。求无限和把上限换成 ooS_inf summation(a * r**k, (k, 0, oo)) print(S_inf)返回结果里会出现 a/(1 - r)并且带上 Abs(r) 1 的收敛条件。如果你关心的只是实数情形可以给 r 加一个 realTrue 的假设输出会稍微简化一些。符号计算的好处是你不需要记住条件它会替你写上坏处是有时候表达式太复杂反而不好读所以通常只在小规模推导时用。再补一个精确验证用 Rational 避开浮点from fractions import Fraction partial sum(Fraction(1, 2)**k for k in range(100)) print(partial) # 一个巨大的分数 print(float(partial)) # 1.0 print(2 - partial) # 2**-99结果非常干净(1/2)^99 这一项被完整保留下来两个分数相减得到的精确值就是 2^-99大约 1.58e-30。这就是有理数运算的价值它把误差有多大变成了一个可以精确写出来的数而不是一句大约很小。3.2 数值逼近观察部分和序列怎么走向极限符号结果确定之后换成 numpy 做数值实验用来观察收敛过程。先写一个最直接的部分和函数import numpy as np def geom_partial(a, r, n): k np.arange(n 1) return np.sum(a * r**k)对 a 1、r 0.5真实和是 2。取不同 n 观察误差| n | 部分和 S_n | 误差 |S - S_n| | | --- | --- | --- | | 5 | 1.96875 | 3.125e-2 | | 10 | 1.9990234375 | 9.766e-4 | | 20 | 1.9999990463256836 | 9.537e-7 | | 30 | 1.9999999990686774 | 9.313e-10 | | 50 | 2.0 | 0达到双精度分辨率 |误差每一行都缩小到前一行的约 1/32对应每加 5 项乘 0.5^5 1/32和理论完全一致。到 n 50 时误差已经在双精度机器精度以下算出来就是 2.0再增加项数没有意义。对 r 0.9 重做一遍感受一下速度差异n 50 时误差约 0.9^51/0.1 ≈ 4.6e-3n 152 时误差约 1e-6n 500 时误差约 1e-23已经到机器精度以下。同一个函数换个公比项数差了一个数量级。再看负公比 r -0.5部分和在 2 上下跳动n 为偶数时略小于 2n 为奇数时略大于 2跳跃幅度以 0.5 的幂衰减。这个振荡是数学上的必然不是数值不稳定。很多人在调试时看到结果来回摆就怀疑代码有问题其实是被负公比骗了。3.3 浮点陷阱三个真实的翻车场景第一个陷阱是 r 极其接近 1 时的抵消误差。看这段代码r 1 - 1e-16 print(r 1.0) # True原因是双精度浮点的机器 epsilon 约 2.22e-161e-16 小于它的二分之一1 - 1e-16 在舍入后就是 1.0这个数根本没有被表示出来。既然 r 已经被舍入成 1后面所有公式算出来的都是 r 1 的结果误差 100%。这个例子有点极端但含义很重要输入精度决定了输出的天花板你不能指望一个已经被舍入掉的参数还能算出正确的和。稍微退一步取 r 1 - 1e-12能表示出来了但 1 - r 这个减法会损失有效位。1 - 1e-12 的十进制表示大约有 12 个有效位被用掉来表达那个 1e-12 的差值剩下的有效位才用来表达差值本身相对误差被放大。用 Fraction 或者 Decimal 一试就能看出来from decimal import Decimal, getcontext getcontext().prec 50 x Decimal(1e-12) r Decimal(1) - x one_minus_r Decimal(1) - r print(one_minus_r) # 1E-12在 Decimal 里是准的在 Decimal 里这个减法是精确的因为十进制表示天然适合表达 10 的幂次差值。用 double 就会丢位。工程上的对策不是换 Decimal太慢而是从一开始就把 r 存成 1 - x 里的那个 x别存 r 本身。需要算 1 - r 的时候直接用 x需要算 r^k 的时候用 expm1/log1p 组合把抵消误差绕过去。第二个陷阱是大 n 累加的舍入。朴素循环 sum(...) 在 n 达到千万级时每一项的舍入误差会累积而且小项加到大的累加器上会被直接吞掉。对策是 math.fsum它做精确舍入的求和误差不随项数增长。或者用 numpy.sum它内部是成对求和误差增长是对数级的比朴素循环好得多但不如 fsum。实测下来n 1e7、r 0.9 时朴素循环和 fsum 的结果能差到 1e-10 量级对精度要求高的场景这个差值是致命的。第三个陷阱是溢出和下溢。r 1 且 n 很大时rn 会抛 OverflowErrorPython 的 float 类型不会静默返回 inf而是直接报错。反过来 r 很小、n 很大时rn 下溢到 0.0这倒没什么危害因为连续加 0 不影响结果。但如果你在算 a·r^n/(1-r) 这种表达式下溢成 0 之后的除法会出现 0/0 或者得到一个错误的 0需要提前判断。注意r**n 在 Python 里对 float 底数用的是 pow精度比 exp(n*log(r)) 好别为了优化手动改成 exp/log 组合除非你就是为了绕开抵消误差那也得配合 log1p 用。3.4 写一个能直接抄作业的稳健函数把前面的坑都兜住给出一版可以放进工具箱的实现。核心思路是分支处理 r 0 和 r 1有限和用 fsum无限和检查 |r| 1靠近 1 的时候走 expm1/log1p 路径。import math def geom_sum(a, r, nNone): 几何级数求和。n 为 None 时求无限和要求 abs(r) 1。 if r 0: return a if n is None else a if n is None: if abs(r) 1: raise ValueError(f无限和发散或未定义, r{r}) return a / (1 - r) if n 0: raise ValueError(n 必须非负) if r 1: return a * (n 1) return math.fsum(a * r**k for k in range(n 1))无限和那一支的 r 0 判断其实可以省掉1 - 0 1a/1 a结果一样。但有限和的 r 0 分支有价值因为它避免了 0**0 的语义依赖虽然 Python 返回 1 恰好是对的但显式写出来更清楚。针对 r 接近 1 的稳定版本单独处理实数且 0 r 1 的情形def geom_partial_stable(a, r, n): 针对 r 接近 1 的部分和减少 1-r 的抵消误差。 if r 1: return a * (n 1) if r 0 or r 1: # r 为负或偏离 1 较远时常规公式没有抵消问题 return a * (1 - r**(n 1)) / (1 - r) x 1 - r # 若能直接传入 x 更好 num -math.expm1((n 1) * math.log1p(-x)) return a * num / x这里的两个技巧值得展开。log1p(-x) 计算的是 ln(1-x)当 x 很小时它不丢失精度而 math.log(1-x) 会先做 1-x 的减法再取对数精度已经损失了。expm1(y) 计算的是 e^y - 1当 y 接近 0 时同样能保持精度而 math.exp(y) - 1 在 y 很小时会得到 0。两个函数配合就把 1 - r^{n1} 这个容易抵消的表达式换成了数值稳定的形式。配套的项数估算函数也写出来注意 log 那里同样要用 log1pdef terms_for_tolerance(a, r, eps): 估算达到 eps 精度所需的项数 n。 ra abs(r) if a 0: return 0 if not (0 ra 1): raise ValueError(仅对 0 |r| 1 有意义) target eps * (1 - ra) / abs(a) # log(ra) 在 ra 接近 1 时用 log1p 更稳 log_r math.log1p(ra - 1) if ra 0.5 else math.log(ra) n math.log(target) / log_r - 1 return max(0, math.ceil(n))对 a 1、r 0.9、eps 1e-6 调用返回 152和手算一致r 0.99 返回 1832r 0.999 返回 20712。这三个数和我前面表格里的值完全对得上说明估算逻辑没问题。4. 工程里真正会用到几何级数的地方4.1 现金流折现与等额本息贷款先看一个最直观的场景。假设年贴现率 5%每年末收到 100 元一直收下去现值是多少第 k 年的现金流折现因子是 1/1.05^k (1/1.05)^k这是一个公比 r 1/1.05 ≈ 0.95238 的几何级数和是 100/(1 - 0.95238) 100/0.04762 2100 元。换个算法永续年金公式是 现金流/利率 100/0.05 2000 元。两个结果差了 100原因是我这里用了年末支付和年初支付的区别年初支付要多乘一个 1.05。这类首项错位在财务计算里极常见本质就是第 1 节说的下标从 0 还是从 1 开始。再看有限期。同样每年 100 元、共 20 年现值是 100·(1 - 1.05^{-20})/0.05。1.05^20 2.6533倒数是 0.376891 减它等于 0.62311乘 100 再除以 0.05得 1246.2 元。和无限期的 2000 元一比能看出第 20 年之后的现金流折现后只剩 750 多元年限拉长的边际贡献衰减得很快这正好对应几何级数尾部项越来越小。等额本息月供是同一套东西的变形。设贷款本金 P、月利率 i、期数 N每月还款 M那么所有还款的现值之和等于本金M·Σ_{k1}^{N} (1i)^{-k} P。里面的和是首项 (1i)^{-1}、公比 (1i)^{-1} 的几何级数化简得 M P·i/(1 - (1i)^{-N})。拿一笔 100 万、年利率 4.8%、20 年期的贷款算一遍。月利率 i 0.048/12 0.004N 240。(1.004)^240 e^{240×ln1.004} e^{0.958085} 2.60634倒数是 0.383661 减它等于 0.61634。M 1e6 × 0.004 / 0.61634 4000/0.61634 ≈ 6490 元。总还款约 155.8 万其中利息约 55.8 万。这个数字你可以用任意贷款计算器核对误差在几十元以内差的那点来自四舍五入。提示这类计算里 i 通常很小0.004 这种量级(1i)^{-N} 可以直接算不会有抵消问题。真正要小心的是利率极低、期数极大同时出现的情形那时候 1-(1i)^{-N} 会接近 1反而更稳不用特殊处理。4.2 算法分析里的几何分布与期望次数几何分布是几何级数的概率版本独立重复试验每次成功概率 p首次成功所需的试验次数 X 服从几何分布P(X k) (1-p)^{k-1}·p。它的期望推导要用到几何级数的求导性质。从 Σ_{k0}^{∞} x^k 1/(1-x) 出发两边对 x 求导得 Σ_{k1}^{∞} k·x^{k-1} 1/(1-x)²。把 x 1-p 代进去再乘上 p就是 E[X] p·1/(1-(1-p))² 1/p。同理再求一次导能得到二阶矩最终方差是 (1-p)/p²。这个 1/p 在工程里出现的频率高得惊人。哈希表的开放寻址负载因子 α 时成功查找的期望探测次数约为 (1/α)·ln(1/(1-α))线性探测则近似 (1/2)(1 1/(1-α))。α 0.75 时后者约 2.5 次这就是为什么主流哈希表把负载因子上限设在 0.75 附近再高一点探测次数就按 1/(1-α) 的形态爆炸。再举个重试策略的例子。某个远程调用单次成功率 0.8失败就重试最多重试 3 次。总尝试次数的期望可以按几何分布截断算E Σ_{k1}^{3} k·0.2^{k-1}·0.8 4·0.2³。算一下1×0.8 2×0.16 3×0.032 4×0.008 0.8 0.32 0.096 0.032 1.248 次。加上截断项之后期望只比 1/p 1.25 小一点点说明重试上限设 3 次基本够用再往上加对成功率贡献很小因为尾部是 0.2 的三次方量级。二分查找的判定树节点数是 1 2 4 … 2^h 2^{h1} - 1这是 r 2 的有限几何级数。这里 r 1无限和发散但有限和完全正常。这个对比说明一件事发散不等于没用发散只意味着你不能把无限多项相加当成一个有限数而有限项求和在 r 1 时照样是标准工具。4.3 马尔可夫链、幂迭代与衰减系数把标量换成矩阵几何级数变成 Neumann 级数Σ_{k0}^{∞} P^k (I - P)^{-1}收敛条件是 P 的谱半径小于 1。这个推广不是数学游戏PageRank 的幂迭代就是这么工作的每一轮把概率向量乘以转移矩阵误差按主特征值之外的次大特征值的模长衰减。如果次大特征值的模长是 0.85那么迭代 100 轮之后误差约 0.85^100 e^{100×ln0.85} e^{-16.25} ≈ 8.7e-8。看着很小但要注意这是从初始误差 1 开始算的如果初始误差是 1e-3最终只能到 1e-10 量级。想再降一个数量级得再来 15 轮左右因为 0.85^15 ≈ 0.087差不多缩小一个数量级。这就是收敛因子接近 1 时迭代成本急剧上升的具体体感。同样的分析适用于任何一个线性迭代 x_{k1} A x_k b。收敛速度由 A 的谱半径决定误差每一轮乘谱半径累积 k 轮就是谱半径的 k 次方。你完全可以把谱半径理解成那个公比 r把整个收敛过程理解成一个矩阵版的几何级数。这套语言在控制系统、强化学习的值迭代、数值线性代数的迭代解法里是通用的。注意谱半径小于 1 是充分必要条件但数值上判断是否真的收敛还要看舍入误差。谱半径 0.9999 的迭代在数学上收敛跑一百万轮才降一个数量级实际工程里通常直接判定为不收敛改用别的算法。4.4 一阶滤波与动画缓动信号处理里的一阶低通滤波y[n] α·y[n-1] (1-α)·x[n]其中 0 α 1。它的冲激响应是 h[k] (1-α)·α^k正好是个几何级数。所有冲激响应之和 Σ h[k] (1-α)/(1-α) 1这个和为 1保证了直流增益为 1输入常量信号时输出稳态也是同一个常量。如果系数写错比如写成 y[n] α·y[n-1] x[n]和就变成 1/(1-α)直流增益被放大输出会缓慢爬升到错误的值。这个 bug 我在调参时见过不止一次。阶跃响应的形式是 1 - α^{n1}达到 99% 所需步数由 α^{n1} ≤ 0.01 决定n ≥ ln(0.01)/ln(α) - 1。α 0.9 时ln0.01 -4.605ln0.9 -0.10536比值 43.7所以需要 44 步。α 0.99 时需要 459 步。这和前面项数估算表是同一个公式只是符号换了个名字。做前端动画的时候缓动曲线的手感其实就是这个衰减系数α 越大越黏收敛越慢。再说一个概率上的用法几何涂鸦、抛硬币直到出现正面期望次数是 2。这个 2 就是 1/p 在 p 0.5 时的值。如果你在做 A/B 测试的样本量估算看到需要多少次曝光才能观察到一次转化这类问题答案就是 1/转化率背后的数学就是几何分布的期望而几何分布的期望又来自几何级数求导。一层套一层但根子是同一个。5. 常见问题与排查清单5.1 症状、根因与处理对照表现象可能根因排查方法处理方式结果恰好差一个 r 倍首项下标从 0 还是从 1 开始搞混手算前两项和代码输出对比统一从 k0 写公式配套改r 1 时抛 ZeroDivisionError通用公式分母 1-r 为零打印 r 的实际值入口处加 r 1 分支r 接近 1 时结果偏差巨大1-r 的抵消误差或 r 已被舍入为 1用 Decimal 高精度复算直接传 x 1-r用 expm1/log1p大 n 时结果与理论值差 1e-10浮点累加舍入换 math.fsum 对比用 fsum 或 numpy.sum无限和返回 inf 或 nanr≥ 1 导致 r**n 溢出负公比时结果来回摆正常振荡不是 bug观察偶数项和奇数项确认是数学性质即可复数公比报类型错误用了 math 模块而非 cmath检查导入用 cmath 或直接用 complex 运算项数估算比实际需要的多很多误差界是保守上界实测截断误差可接受或改用实测值这张表里最值得反复看的是第三行。r 接近 1 的偏差往往不是算得不够多而是参数精度不够多加项数完全无效。很多人第一反应是把 n 加大结果算了十万项还是不对因为瓶颈在输入而不是迭代。5.2 几个我踩过的坑和对应的处理习惯第一个坑是 0.999… 到底等不等于 1。写成代码是 sum(9 * 10**-k for k in range(1, 100))用浮点算出来是 0.9999999999999999不等于 1。换 Fraction 算结果是精确的 1。这个差异不是数学问题是表示问题十进制小数 0.9 在二进制浮点里没有精确表示累积误差让结果偏了一点。我的处理习惯是涉及精确相等判断的场景一律用 Fraction 或 Decimal绝不拿浮点结果去和 1 比 。第二个坑是 numpy 的整数溢出。写 np.arange(k) ** 64 或者对 int32 数组做大指数幂会静默溢出成负数不报错。我现在的习惯是任何幂运算前先把数组转成 float64 或者 object宁可慢一点也不要静默错误。对几何级数来说r 是整数的情况其实很少但一旦出现比如算 2 的幂次和就是重灾区。第三个坑是 math.log 在参数接近 1 时的精度。math.log(1 - 1e-15) 和 math.log1p(-1e-15) 的结果能差出好几个有效位前者因为先做了减法精度已经丢了。凡是形如 ln(1x) 的表达式只要 x 可能很小一律用 log1p凡形如 e^x - 1 的表达式一律用 expm1。这两条规则我写在代码规范里比事后调 bug 省事得多。第四个坑是误用无限和公式。有人拿着 a/(1-r) 去算一个本该分期的现金流忘了期限是有限的结果把 20 年的现值算成了永续的多出 50% 以上。判断方法很简单问一句这个流有没有终点有终点就用有限和公式没有才用无限和。有限和公式对任意 r ≠ 1 都成立适用范围比无限和宽得多。第五个坑是符号计算和数值计算混用。sympy 给的是精确表达式numpy 给的是浮点近似两者对比时不能用 。我一般的做法是把符号结果转成 float 再和数值结果比容差设成 1e-12 量级考虑累积误差超过就说明有一边错了。这个对比步骤在写新的求和函数时几乎必做是发现下标错位最快的手段。最后再分享一个小习惯每次写几何级数相关的函数我都会在函数上方留一行注释写清楚k 从 0 开始首项是 a。看起来是废话但半年后回来改代码这一行能省掉十分钟的推导。数学上的下标约定永远比你以为的更容易忘。