第 1 章 微積分的誕生:從窮竭法到極限#
1.3 SymPy 的極限語言:limit、seq、series 的第一課#
學習動機#
前兩節講的是概念從哪裡來:阿基米德給出「可證的逼近」,牛頓與萊布尼茲給出「變化率」。這一節要處理的是一個更實際的問題:當我們要真的去計算一個極限時,人和機器各自的處境是什麼?
這裡有一個深刻的對照:
- 人類的極限定義是「先定義再驗證」。我們先寫下 \(\varepsilon\) - \(\delta\) 的量詞結構(這是魏爾斯特拉斯在 1872 年做到的),然後證明某個 \(\delta\) 存在。極限定義是「標準」,計算是「應用」。
- SymPy 的極限是「先猜再驗證」。它用級數展開與洛必達法則等啟發式規則把極限「化簡」成一個已知的基本極限,然後回報答案。整個過程沒有從定義出發。
這個差別不是吹毛求疵。它決定了你在使用電腦計算時應該問什麼問題:
| 人類 | SymPy | |
|---|---|---|
| 出發點 | \(\varepsilon\) - \(\delta\) 定義 | 已知的「基本極限」表 |
| 過程 | 構造 \(\delta\) 並證明其存在 | 代數化簡 + 級數展開 + 洛必達 |
| 產出 | 證明(在定理體系中) | 數值(可信的線索,但獨立的證明) |
| 失敗時 | 「我不知道」 | Limit(...) 未求值物件、或 AccumBounds(-1, 1) |
| 侷限 | 慢、常靠靈感 | 快、但不保證極限存在,也不保證答案是對的 |
SymPy 算不出來的極限,不代表極限不存在(例如 \(\lim_{x \to 0} e^{-x^2}\sin\frac{1}{x}\) 的真值是 \(0\) ,但 SymPy 只敢給出 \(\text{AccumBounds}(-1, 1)\) )。這是本節要建立的計算機時代的數學素養。
本節的技術內容是四件工具:sp.limit、sp.Limit、sp.limit_seq / sp.sequence、sp.summation / sp.Sum / sp.Product / sp.series(含 dir 引數)。我們用它們把 \(\lim_{n \to \infty}\left(1 + \frac{1}{n}\right)^n\) 、 \(\lim_{x \to 0}\frac{\sin x}{x}\) 這兩個「現代微積分的招牌極限」算一遍,最後再用 \(\varepsilon\) - \(\delta\) 的方式親手驗證其中一個,作為「先定義再驗證」的示範。
1.3.1 從「趨近」到「定義」:極限觀念的三個里程碑#
在我們寫第一行 SymPy 程式之前,值得先建立歷史上的坐標系。極限觀念的成熟經歷了三個明確的里程碑:
| 年份 | 人物 | 貢獻 | 留下的缺口 |
|---|---|---|---|
| 1817 | 玻爾查諾(Bernard Bolzano) | 首次用「連續」重新表述函數的連續性,並指出「趨近」需要一個精確的語言 | 仍以口語描述為主,沒有公理化的量化結構 |
| 1821 | 柯西(Augustin-Louis Cauchy) | 在《分析原理》中首次把極限寫成不等式: \(\frac{1}{k}\|\text{差}\| < \varepsilon\) | 仍以「變量趨近於某值」為起點,未處理實數的存在性 |
| 1872 | 魏爾斯特拉斯(Karl Weierstrass) | 給出現代的 \(\varepsilon\) - \(\delta\) 定義 | 需要實數系的嚴格構造(第 12 章) |
柯西的定義長這樣(用他自己的符號 \(x\) 表示變量、 \(x_0\) 表示極限點):
當變量 \(x\) 趨近於 \(x_0\) 時,若對於任意 \(\varepsilon > 0\) ,存在 \(\delta > 0\) 使得 \(0 < |x - x_0| < \delta\) 時恆有 \(|f(x) - f(x_0)| < \varepsilon\) ,則稱 \(f(x)\) 趨近於 \(f(x_0)\) 。
這幾乎就是今天的定義。柯西的貢獻是把「無限接近」這種直覺,翻譯成「對每個 \(\varepsilon\) 都存在 \(\delta\) 」這種可以逐條檢查的邏輯結構。
而魏爾斯特拉斯的版本(去掉「 \(f(x_0)\) 」改寫成極限值 \(L\) )我們在 1.1.3 節已經寫過。這三步就是第 2 章的內容。 本節的重點是:SymPy 活在這條時間線的哪裡?
答案是:它活在柯西之前。 SymPy 的 limit 走的是「化簡到已知極限」的路線,與魏爾斯特拉斯的「從定義出發構造證明」是兩種不同的推理模式。理解這一點,我們才知道該如何解讀它的輸出。
1.3.2 sp.limit 與 sp.Limit:SymPy 的極限介面#
SymPy 的極限介面非常簡潔:
sp.limit(expr, z, z0) # 計算極限
sp.limit(expr, z, z0, dir='+') # dir='+'(預設)、'-'、'+-'
sp.Limit(expr, z, z0) # 建立「未求值」的極限物件
sp.Limit 建立的物件可以呼叫 .doit() 來嘗試求值,也可以 .subs() 參數。這種「先建立物件、後求值」的設計(與 sp.Integral、sp.Derivative、sp.Sum 一致)是 SymPy 的核心風格:每一個數學概念都是一個可以操作、可以印出、可以查詢的物件。
import sympy as sp
x = sp.symbols('x', real=True)
n = sp.symbols('n', integer=True, positive=True)
print(sp.limit(sp.sin(x) / x, x, 0)) # 1
print(sp.limit(1 / x, x, 0)) # oo (預設從右側逼近)
print(sp.limit(1 / x, x, 0, dir='-')) # -oo (從左側逼近)
print(sp.limit(sp.log(x) / x, x, 0)) # -oo
L = sp.Limit(sp.sin(x) / x, x, 0) # 尚未求值的極限物件
print(L)
print(L.doit()) # 1
print(sp.limit(sp.exp(-x**2) * sp.sin(1 / x), x, 0)) # SymPy 只能給出界
f = sp.Function('f')
print(sp.limit(f(x), x, 0)) # 未求值:SymPy 不會瞎猜
實際輸出:
1
oo
-oo
-oo
Limit(sin(x)/x, x, 0, dir='+')
1
AccumBounds(-1, 1)
Limit(f(x), x, 0, dir='+')
逐行解讀:
sp.limit(sp.sin(x)/x, x, 0)回傳1。這就是招牌極限。dir引數決定從哪一側逼近。預設是'+'。 \(\lim_{x \to 0}\frac{1}{x}\) 從右側是 \(+\infty\) 、從左側是 \(-\infty\) ,雙側極限不存在。SymPy 會如實回報左右不一致,而不是給你一個假的答案。sp.Limit(...)印出Limit(sin(x)/x, x, 0, dir='+'):一個還沒算的極限。.doit()才會去算。sp.limit(sp.exp(-x**2) * sp.sin(1 / x), x, 0)回傳AccumBounds(-1, 1)——這是本節最重要的一行!真實的極限是 \(0\) (因為 \(e^{-x^2} \to 0\) ,而 \(|\sin \frac{1}{x}| \le 1\) ),但 SymPy 的機制無法證明極限存在於 \(0\) ,所以它只敢給出「一定被夾在 \(-1\) 與 \(1\) 之間」這個誠實的弱結論。sp.limit(f(x), x, 0)回傳未求值的Limit(f(x), x, 0, dir='+'):對一個未知的函數 \(f\) ,SymPy 拒絕猜測。
這兩行是 SymPy 最值得學習的態度。 它寧可回傳「我只知道這麼多」,也不願意編一個看起來合理的答案。回想 1.1.1 節希臘人的「不可解」概念——兩千兩百年後,計算機終於學會了說「我不知道」。
1.3.3 數列、級數與 dir 引數:符號運算的四件套#
極限的對象常常是數列而非函數。SymPy 為此提供了一組平行工具:
| 工具 | 用途 | 備註 |
|---|---|---|
sp.limit_seq(expr, n) | 數列 \(\lim_{n \to \infty} a_n\) | SymPy 1.14 的正式名稱 |
sp.sequence(expr, (n, 0, oo)) | 建立可索引的序列物件 | 支援 s[3]、s[0:5] |
sp.summation(expr, (n, a, b)) | 級數求和(自動求值) | 含 \(b = \infty\) 的情形 |
sp.Sum(expr, (n, a, b)) / .doit() | 級數物件(先建後算) | 與 sp.Limit 風格一致 |
sp.Product(expr, (n, a, b)) / .doit() | 連乘物件 | \(\prod\) 的物件版 |
sp.series(expr, x, x0, n, dir=...) | 泰勒/勞倫展開 | dir 指定展開方向 |
dir 引數在 series 上特別重要:展開方向不同,結果可能完全不同,甚至會牽涉到複數分支。下面這段程式碼把這件事顯示得清清楚楚:
import sympy as sp
n = sp.symbols('n', integer=True, positive=True)
k = sp.symbols('k', integer=True, nonnegative=True)
x = sp.symbols('x', real=True)
# 數列的極限:limit_seq(SymPy 1.14 的正式名稱)
print(sp.limit_seq(n, n)) # oo
print(sp.limit_seq(sp.Rational(1, 2)**n, n)) # 0
print(sp.limit_seq((1 + 1 / n)**n, n)) # E
# 序列物件
s = sp.sequence(n**2, (n, 0, sp.oo))
print(s, s[3], s[0:5])
# 級數
print(sp.summation(1 / n**2, (n, 1, sp.oo))) # pi**2/6
print(sp.summation(sp.Rational(1, 2)**n, (n, 1, sp.oo))) # 1
# Sum / Product 物件
print(sp.Sum(sp.Rational(1, 4)**k, (k, 0, sp.oo)).doit()) # 4/3
print(sp.Product((n + 1) / n, (n, 1, 5)).doit()) # 6
# 級數展開與 dir 引數
print(sp.series(sp.sin(x), x, 0, 6))
print(sp.series(sp.log(x), x, 0, 3, dir='-'))
實際輸出:
oo
0
E
SeqFormula(n**2, (n, 0, oo)) 9 [0, 1, 4, 9, 16]
pi**2/6
1
4/3
6
x - x**3/6 + x**5/120 + O(x**6)
log(-x) + I*pi
幾點說明:
sp.limit_seq((1 + 1 / n)**n, n)回傳E——SymPy 的E就是歐拉數 \(e \approx 2.71828\ldots\) 。注意它不是exp(1),而是符號物件E;sp.E與sp.exp(1)在 SymPy 內部是同一個東西,但E的印法更短。sp.summation(1/n**2, (n, 1, sp.oo))回傳pi**2/6:巴塞爾問題!這是歐拉 1735 年用微積分算出來的。注意這是符號運算的成果—— \(e\) 、 \(\pi\) 、 \(\zeta(2)\) 三者被放進同一個式子裡。sp.Product((n+1)/n, (n, 1, 5)).doit()回傳6:這個望遠鏡乘積 \(\frac{2}{1}\cdot\frac{3}{2}\cdot\frac{4}{3}\cdot\frac{5}{4}\cdot\frac{6}{5} = 6\) 是微積分基本定理的原型案例( \(e^x\) 的乘積極限,第 6 章)。- 最後一行是本段最重要的一行:
sp.series(sp.log(x), x, 0, 3, dir='-')回傳log(-x) + I*pi,而不是log(x)。因為從左側 \(x \to 0^-\) 逼近時,實對數沒有定義;SymPy 沿著複數的支(branch)往上走,得到 \(\log x = \log(-x) + i\pi\) 。這不是 bug,而是真實的數學:複對數在負實軸上有分支切口。「從哪一邊逼近」不只是收斂速度的技術細節,它會改變答案本身。
1.3.4 「先猜再驗證」:SymPy 的極限到底怎麼算的#
現在來回答本節的主問題。SymPy 的 sp.limit 底下跑的是 sympy.series.gruntz 演算法(Bruno Gruntz 於 1997 年提出),它的邏輯大致是:
- 嘗試把極限化簡成已知的基本極限(例如 \(\lim_{x \to 0}\frac{\sin x}{x} = 1\) 、 \(\lim_{x\to 0}(1+x)^{1/x} = e\) 、 \(\lim_{x\to\infty}\frac{\ln x}{x} = 0\) );
- 化簡的手段包括泰勒展開、洛必達法則(對 \(0/0\) 與 \(\infty/\infty\) 型)、極限的乘除與冪次法則;
- 若化簡無法收斂,則回報失敗——留下未求值的
Limit物件,或給出一個弱一點的結論(例如AccumBounds)。
讓我們直接看這三種結果。
import sympy as sp
from sympy.series.gruntz import gruntz # SymPy 極限機制的核心引擎
x = sp.symbols('x', real=True)
n = sp.symbols('n', integer=True, positive=True)
# 1) 成功:化簡到已知的基本極限
print(sp.limit(sp.sin(x) / x, x, 0))
print(sp.limit((1 + 1 / n)**n, n, sp.oo))
# 2) 失敗但不說謊:回傳一個界
print(sp.limit(sp.exp(-x**2) * sp.sin(1 / x), x, 0))
# 3) 完全無法處理:保留未求值物件
f = sp.Function('f')
print(sp.limit(f(x), x, 0))
# 直接呼叫底層引擎,看它對 (1+1/n)^n 是怎麼處理的
print(gruntz((1 + 1 / n)**n, n, sp.oo))
實際輸出:
1
E
AccumBounds(-1, 1)
Limit(f(x), x, 0, dir='+')
E
(輸出的第二行與最後一行,分別是 sp.limit 與底層 gruntz 對同一個極限 \(\left(1 + \frac{1}{n}\right)^n\) 的答案:都是 E 。但 sp.limit 會先做各種前置最佳化,例如檢查極限點的符號假設、化簡極限表達式等,所以它的介面比 gruntz 友善得多。)
現在我們可以精確地說出「先猜再驗證」的意思了:
- 「猜」:SymPy 假設這個極限可以化簡到某個已知極限,然後沿著這個假設一路做代數變換。
- 「驗證」:SymPy 在過程中不斷檢查每一步變換的合法性(例如「這一步除以了一個趨於零的量,所以需要另外的判斷」)。當檢查不過,它就停下來。
- 它不做的事:它不從 \(\varepsilon\) - \(\delta\) 定義出發去構造證明。 它的輸出是一個答案,不是一個證明。
所以正確的使用態度是:
SymPy 的
limit是「先猜再驗證」的自動化;你的工作是把它的輸出升級成「先定義再驗證」的證明。
下面的 1.3.6 節就示範如何升級。
1.3.5 實戰:三個招牌極限#
現在我們把前面學到的工具全部用上,算出現代微積分的兩個招牌極限,並示範對數化簡這個極其重要的技巧。
對數化簡的想法是: \(\lim_{x \to \infty} a(x)^{b(x)}\) 這種形式很難直接算,但取對數之後
變成了一個乘積極限,通常簡單得多。算出 \(\log\) 的極限是 \(L\) 之後,再取指數回來就是原來的極限 \(e^L\) 。這個技巧是萊布尼茲記號的直接受益者——因為 \(\log y = \frac{\ln y}{y}\) ,天然是「對數」與「指數」的組合。
import sympy as sp
n = sp.symbols('n', integer=True, positive=True)
x = sp.symbols('x', real=True)
# ---- e 的定義:e = lim (1 + 1/n)^n ----
a = (1 + 1 / n)**n
print("(1+1/n)^n 的極限 =", sp.limit(a, n, sp.oo))
print("級數展開 =", sp.series(a, n, sp.oo, 4))
# ---- 對數化簡:log a = n log(1+1/n) -> 1,再取 exp 得 e ----
print("log((1+1/n)^n) =", sp.expand_log(sp.log(a)))
print("log((1+1/n)^n) 的極限 =", sp.limit(sp.log(a), n, sp.oo))
print("exp(1) =", sp.exp(1), "=", sp.N(sp.E, 15))
print("極限值與 exp(1) 相同 :", sp.limit(sp.log(a), n, sp.oo) == 1)
# ---- 極限與對數互換的「可驗證」版本 ----
print("n log(1+1/n) - 1 的極限 =", sp.limit(n * sp.log(1 + 1 / n) - 1, n, sp.oo))
print("用級數估計誤差 =", sp.series(n * sp.log(1 + 1 / n) - 1, n, sp.oo, 4))
# ---- sin x / x ----
print("sin(x)/x 的極限 =", sp.limit(sp.sin(x) / x, x, 0))
print("tan(x)/x 的極限 =", sp.limit(sp.tan(x) / x, x, 0))
實際輸出:
(1+1/n)^n 的極限 = E
級數展開 = -7*E/(16*n**3) + 11*E/(24*n**2) - E/(2*n) + E + O(n**(-4), (n, oo))
log((1+1/n)^n) = n*log(1 + 1/n)
log((1+1/n)^n) 的極限 = 1
exp(1) = E = 2.71828182845905
極限值與 exp(1) 相同 : True
n log(1+1/n) - 1 的極限 = 0
用級數估計誤差 = -1/(4*n**3) + 1/(3*n**2) - 1/(2*n) + O(n**(-4), (n, oo))
sin(x)/x 的極限 = 1
tan(x)/x 的極限 = 1
「對數化簡」的完整邏輯鏈在這裡一目了然:
sp.limit(a, n, sp.oo)直接给出E。- 但我們不滿足於這個黑箱。取對數:
log((1+1/n)^n) = n*log(1 + 1/n)(sp.expand_log做的正是這個代數化簡)。 sp.limit(sp.log(a), n, sp.oo)給出1——這個極限容易得多,因為 \(\log(1 + t) \sim t\) ,所以 \(n\log\left(1 + \frac{1}{n}\right) \to 1\) 。- 取指數回來: \(e^1 = e\) 。
sp.limit(sp.log(a), n, sp.oo) == 1回傳True,確認了這一步的精確性。 - 最後用級數估計誤差:
n log(1+1/n) - 1的展開告訴我們誤差是 \(O\left(\frac{1}{n}\right)\) (主項 \(-\frac{1}{2n}\) ),所以 \(n\) 大約 \(10^6\) 時就有 \(10^{-6}\) 的精度。
這就是「先猜再驗證」的完整實現:SymPy 給出答案(猜),我們用級數估計誤差(驗證),並且用一條獨立的路徑(對數)交叉核對。
順帶注意 sin(x)/x 與 tan(x)/x 都給出 1——這三個「基本極限」正是 SymPy 內部極限表的骨架,也是 Gruntz 演算法反覆回到的標準形。
1.3.6 把「猜」升級成「證」: \(\varepsilon\) - \(\delta\) 示範#
現在做本節最重要的事:用 SymPy 輔助,把 \(\lim_{x \to 0}\frac{\sin x}{x} = 1\) 從一個「計算結果」升級成一個「證明」。
回顧證明的骨架(第 2 章會正式展開):
目標: \(\forall \varepsilon > 0, \ \exists \delta > 0: 0 < |x| < \delta \Rightarrow \left|\frac{\sin x}{x} - 1\right| < \varepsilon\) 。
關鍵估計:對 \(x > 0\) 有
於是
取 \(\delta = \min\left(1, \sqrt{3\varepsilon}\right)\) ,則 \(\delta \le 1\) 且 \(\frac{\delta^2}{6} \le \frac{3\varepsilon}{6} = \frac{\varepsilon}{2} < \varepsilon\) 。
證明關鍵估計(這才是需要動腦的部分):令
則 \(g'''(x) = 1 - \cos x \ge 0\) (因為 \(\cos x \le 1\) ),所以 \(g''\) 遞增; \(g''(0) = 0\) 故 \(g'' \ge 0\) ; \(g'(0) = 0\) 故 \(g' \ge 0\) ; \(g(0) = 0\) 故 \(g \ge 0\) 。而 \(1 - \cos x \ge 0\) 可以由 \(\cos\) 的級數展開看出來。
SymPy 可以把這條鏈子裡的代數部分全部代勞——這正是「計算機輔助證明」的日常版本:
import sympy as sp
import numpy as np
x, eps = sp.symbols('x eps', positive=True)
# 目標:證明 lim(x->0) sin(x)/x = 1
# 關鍵不等式 0 <= x - sin(x) <= x^3/6 (x > 0)
g = x**3 / sp.Integer(6) - x + sp.sin(x)
print("g(x) =", g)
print("g'(x) =", sp.simplify(sp.diff(g, x)))
print("g''(x)=", sp.simplify(sp.diff(g, x, 2)))
print("g'''=", sp.simplify(sp.diff(g, x, 3)))
print("1 - cos(x) 的展開 =", sp.series(1 - sp.cos(x), x, 0, 8))
# 由 |sin(x)/x - 1| <= x**2/6 取 delta = min(1, sqrt(3 eps))
delta = sp.Min(1, sp.sqrt(3 * eps))
print("delta =", delta)
d = float(delta.subs(eps, sp.Rational(1, 10**6)))
print("eps = 1e-6 時 delta =", d)
worst = max(abs(float(sp.N(sp.sin(t) / t - 1))) for t in np.linspace(d / 1000, d, 5000))
print("區間 (0, delta] 上的最大誤差 =", worst)
print("確實小於 eps :", worst < 1e-6)
實際輸出:
g(x) = x**3/6 - x + sin(x)
g'(x) = x**2/2 + cos(x) - 1
g''(x)= x - sin(x)
g'''= 1 - cos(x)
1 - cos(x) 的展開 = x**2/2 - x**4/24 + x**6/720 + O(x**8)
delta = Min(1, sqrt(3)*sqrt(eps))
eps = 1e-6 時 delta = 0.0017320508075688774
區間 (0, delta] 上的最大誤差 = 4.999999250188125e-07
確實小於 eps : True
這段程式碼把「證明」拆成了三塊,每一塊的負責人不同:
| 證明的部分 | 由誰完成 |
|---|---|
| \(g''' = 1 - \cos x\) 、 \(1-\cos x \ge 0\) | SymPy(導數與級數) |
| 由 \(g''' \ge 0\) 推出 \(g \ge 0\) | 人(單調性推理) |
| \(\delta = \min(1, \sqrt{3\varepsilon})\) 的選擇 | 人(策略) |
| 誤差確實小於 \(\varepsilon\) 的抽查 | SymPy + numpy(數值核對) |
這就是「計算機輔助證明」的真正形態:機器負責機械的、可驗證的代數部分,人負責選擇策略與組織推理。 這與 1.1.5 節阿基米德《方法》的精神是相通的——工具的價值不在於替你思考,而在於讓你的思考走得更遠。
順帶一提,sp.ask(sp.Q.positive(sp.sin(x)), ...) 對符號變數的符號問題會回傳 None(無法判定)。這提醒我們:SymPy 能處理「代數上可判定」的問題,但「在整個區間上為正」這種需要人做量詞推理的問題,仍然是人的工作。
SymPy 練習#
練習 1.3.1(dir 引數的全面體驗) 對函數 \(f(x) = \frac{1}{x}\) 、 \(\frac{\log x}{x}\) 、 \(\frac{|x|}{x}\) 、 \(\frac{x}{|x|}\) ,用 sp.limit(..., dir='+')、dir='-' 與 dir='+-' 三種模式計算極限,並解釋每一個結果。特別地, \(\frac{|x|}{x}\) 從右側是 \(1\) 、從左側是 \(-1\) ——請說明為什麼 SymPy 在 dir='+-' 時會怎麼處理,並用你自己的話描述「雙側極限存在」的條件。
練習 1.3.2(SymPy 認不出的極限) 用 sp.limit 計算 \(\lim_{x \to 0} e^{-x^2}\sin\frac{1}{x}\) ,確認它回傳 AccumBounds(-1, 1)。然後自己證明極限存在且等於 \(0\) (用 \(|e^{-x^2}| \le 1\) 與擠壓定理),並說明:SymPy 的引擎(Gruntz)為什麼做不到?提示:它需要處理「高頻振盪」。
練習 1.3.3(數列極限的兩種算法) 用 sp.limit_seq 與 sp.limit 兩種方式計算 \(\lim_{n \to \infty} \frac{2n+1}{3n-2}\) 、 \(\lim_{n\to\infty}\frac{n!}{n^n}\) 、 \(\lim_{n \to \infty}\left(\frac{1}{2}\right)^n\) ,比較兩者的成功率與耗時。找出至少一個 sp.limit_seq 能算而 sp.limit 算不出(或算得慢)的例子,並解釋原因。
練習 1.3.4(series 的 dir 引數與分支) 計算 sp.series(sp.log(x), x, 0, 3) 與 sp.series(sp.log(x), x, 0, 3, dir='-'),解釋輸出差異(提示:複對數的分支)。再計算 sp.series(sp.sqrt(1+x), x, 0, 4) 與 sp.series(sp.sqrt(1-x), x, 0, 4),說明為什麼兩者的奇次項係數互為相反數,而 sp.sqrt 預設取主值(principal value)。
練習 1.3.5(把猜變成證:數列版) 用對數化簡的方法證明 \(\lim_{n \to \infty} \left(1 + \frac{1}{n}\right)^{n} = e\) :先用 sp.limit 算出 \(\lim n\log\left(1+\frac{1}{n}\right)\) ,再用 sp.series 估計誤差,並寫出「 \(\left(1+\frac{1}{n}\right)^n = e^{\,1 - \frac{1}{2n} + O(n^{-2})}\) 」這個估計。最後用 sp.limit_seq 檢查 \(\lim_{n \to \infty}\left[\left(1+\frac{1}{n}\right)^n - e\right]\cdot n\) ,看它是否等於 \(-\frac{e}{2}\) 。
練習 1.3.6(Sum 物件與求和順序) 用 sp.Sum 建立 \(\sum_{k=1}^{n}\frac{1}{k^2}\) ,分別對 (a) 先求和後取極限、(b) 先取極限後求和(這是不合法的,因為 \(n\) 消失了)兩種順序操作,說明 SymPy 會如何反應。第 12 章會正式處理「極限與求和能否交換」這個問題——先在這裡做個預告。
小結#
- SymPy 的極限介面:
sp.limit(expr, z, z0, dir=...)(其中dir為'+'、'-'、'+-')與未求值物件sp.Limit(expr, z, z0).doit()。 - 數列與級數的工具:
sp.limit_seq、sp.sequence、sp.summation、sp.Sum、sp.Product、sp.series(含dir引數)。 - SymPy 的
limit是「先猜再驗證」:核心引擎是 Gruntz 演算法,靠級數展開與洛必達法則化簡到已知的基本極限;它不從 \(\varepsilon\) - \(\delta\) 定義出發,所以它的輸出是答案而非證明。 - SymPy 在失敗時的態度值得學習:回傳
Limit(...)(未求值)或AccumBounds(-1, 1)(弱界),而不是編造答案。 \(\lim_{x\to 0}e^{-x^2}\sin\frac{1}{x}\) 的真值是 \(0\) ,但 SymPy 只敢說「在 \(-1\) 與 \(1\) 之間」。 - 招牌極限: \(\lim_{n\to\infty}\left(1+\frac{1}{n}\right)^n = e\) 、 \(\lim_{x\to 0}\frac{\sin x}{x} = 1\) 、 \(\lim_{x\to 0}\frac{\tan x}{x} = 1\) 。對數化簡是把困難極限變簡單的關鍵技術。
- 把「猜」升級成「證」:SymPy 負責導數、級數、代數化簡與數值抽查;人負責選擇 \(\delta\) 與組織單調性推理。這就是計算機輔助證明的日常形態。
下一節我們將正式進入第 2 章:把極限定義寫成不等式,從柯西到魏爾斯特拉斯,看看「先定義再驗證」的完整工程是如何鋪開的。
本節 SymPy 新工具清單#
| 工具 | 用途 | 例 |
|---|---|---|
sp.limit(expr, z, z0, dir=) | 計算極限 | sp.limit(sp.sin(x)/x, x, 0) |
sp.Limit(expr, z, z0) / .doit() | 未求值的極限物件 | sp.Limit(sp.sin(x)/x, x, 0) |
sp.limit_seq(expr, n) | 數列極限 | sp.limit_seq((1+1/n)**n, n) |
sp.sequence(expr, (n, a, b)) | 可索引的序列物件 | sp.sequence(n**2, (n, 0, sp.oo)) |
sp.summation(expr, (n, a, b)) | 級數求和 | sp.summation(1/n**2, (n, 1, sp.oo)) |
sp.Sum / sp.Product / .doit() | 級數與乘積物件 | sp.Product((n+1)/n, (n,1,5)).doit() |
sp.series(expr, z, z0, k, dir=) | 泰勒/勞倫展開 | sp.series(sp.log(x), x, 0, 3, dir='-') |
sp.expand_log(expr) | 對數的代數化簡 | sp.expand_log(sp.log(a)) |
sp.diff(f, x, k) | 第 \(k\) 階導數 | sp.diff(g, x, 3) |
sp.ask(Q.positive(...), ...) | 嘗試判定符號 | 常常回傳 None |