第 2 章 極限的嚴格化:柯西、魏爾斯特拉斯與 ε-δ#
2.3 用 SymPy 驗證極限定理:夾擠、單調、有界#
學習動機#
前兩節是語言與思想:柯西把極限寫成不等式,魏爾斯特拉斯把分析變成「不等式的演算術」。本節要把它們落到具體的定理上,並且回答一個實務問題:
SymPy 能不能「驗證」定理?
這裡的「驗證」需要先說清楚,因為它很容易被誤解。SymPy 不能:
- 從 \(\varepsilon\) - \(\delta\) 定義出發構造證明;
- 判斷一個極限是否存在(它常常只是猜);
- 證明任何涉及「對所有」的敘述(例如「對所有 \(x > 0\) 成立」)。
SymPy 能:
- 把定理中的每個具體極限算出來,讓你核對結論;
- 用級數展開揭曉誤差的量級(這是構造 \(\delta\) 的關鍵);
- 用不等式求解器把「 \(\lvert f(x) - L\rvert < \varepsilon\) 」變成可操作的區間;
- 在你懷疑定理時,用數值與隨機取樣去尋找反例。
本節的核心訊息只有一句:
SymPy 不能取代定義,但能加速反例搜尋與機械運算。
我們要用三個定理(夾擠、單調有界、Stolz–Cesàro)和四個招牌極限( \(n\sin\frac{1}{n}\) 、 \(\left(1+\frac{1}{n}\right)^n\) 、 \(\frac{2^n}{n!}\) 、 \(\sqrt[n]{n}\) )把這句話實踐一遍。
2.3.1 夾擠定理:SymPy 版本#
夾擠定理(Sandwich / Squeeze Theorem):若
且 \(\lim_{x \to a} g(x) = \lim_{x \to a} h(x) = L\) ,則 \(\lim_{x \to a} f(x) = L\) 。
它的價值在於「把困難的極限夾在兩個容易的極限之間」。 最著名的應用是
因為對 \(0 < x < \frac{\pi}{2}\) 有 \(\cos x \le \frac{\sin x}{x} \le 1\) ,而 \(\cos x \to 1\) 。
SymPy 能做什麼? 三件事:
- 算出兩端的極限(確認它們真的相等);
- 用級數展開驗證不等式(把「 \(\cos x \le \frac{\sin x}{x} \le 1\) 」這個需要人做的推理,變成「兩個差函數的級數主項都為正」);
- 用精確有理數做抽查。
import sympy as sp
# ---- 夾擠定理(Squeeze / Sandwich)----
x, n = sp.symbols('x n', real=True, positive=True)
print("=== 夾擠定理 ===")
print("目標:lim(x->0) sin(x)/x = 1")
print(" 夾擠:cos(x) <= sin(x)/x <= 1 (0 < x < pi/2)")
print(" cos(x) 的極限 =", sp.limit(sp.cos(x), x, 0))
print(" 1 的極限 =", sp.limit(sp.Integer(1), x, 0))
print(" 所以夾擠定理給出 lim sin(x)/x = 1")
print()
t = sp.symbols('t', positive=True)
print(" SymPy 驗證不等式 sin(t) - t*cos(t) >= 0 的級數展開:")
print(" t - sin(t) =", sp.series(t - sp.sin(t), t, 0, 5))
print(" sin(t) - t*cos(t) =", sp.series(sp.sin(t) - t * sp.cos(t), t, 0, 5))
print()
print(" 數值夾擠驗證:")
for xv in [sp.Rational(1, 1000), sp.Rational(1, 100), sp.Rational(1, 10)]:
lo = sp.N(sp.cos(xv), 12)
mid = sp.N(sp.sin(xv) / xv, 12)
hi = sp.Integer(1)
print(f" x = {str(xv):>6}: {lo} <= {mid} <= 1 ? {lo <= mid <= 1}")
print()
# ---- 單調有界定理 ----
print("=== 單調有界定理 ===")
m = sp.symbols('m', integer=True, positive=True)
a_m = 1 - 1 / m
print(" a_m = 1 - 1/m")
print(" a_{m+1} - a_m =", sp.simplify(a_m.subs(m, m + 1) - a_m), " (> 0 所以遞增)")
print(" 1 - a_m =", sp.simplify(1 - a_m), " (> 0 所以 a_m < 1 有界)")
print(" limit_seq =", sp.limit_seq(a_m, m))
實際輸出:
=== 夾擠定理 ===
目標:lim(x->0) sin(x)/x = 1
夾擠:cos(x) <= sin(x)/x <= 1 (0 < x < pi/2)
cos(x) 的極限 = 1
1 的極限 = 1
所以夾擠定理給出 lim sin(x)/x = 1
SymPy 驗證不等式 sin(t) - t*cos(t) >= 0 的級數展開:
t - sin(t) = t**3/6 + O(t**5)
sin(t) - t*cos(t) = t**3/3 + O(t**5)
數值夾擠驗證:
x = 1/1000: 0.999999500000 <= 0.999999833333 <= 1 ? True
x = 1/100: 0.999950000417 <= 0.999983333417 <= 1 ? True
x = 1/10: 0.995004165278 <= 0.998334166468 <= 1 ? True
=== 單調有界定理 ===
a_m = 1 - 1/m
a_{m+1} - a_m = 1/(m*(m + 1)) (> 0 所以遞增)
1 - a_m = 1/m (> 0 所以 a_m < 1 有界)
limit_seq = 1
逐段解讀:
- 夾擠定理的三段:SymPy 先確認兩端極限相等(都是
1),再用級數展開證實兩個「差」的主項都是正的( \(t^3/6\) 與 \(t^3/3\) ),最後用精確有理數抽查三個點。注意這裡的分工:SymPy 負責「算出差函數並看它的主項」,人負責「主項為正且 \(t>0\) 所以差函數在 \(t\) 充分小時為正」的推理。這是計算機輔助證明的標準分工。 - 單調有界定理:SymPy 把 \(a_{m+1} - a_m\) 化簡成 \(\frac{1}{m(m+1)}\) ,把 \(1 - a_m\) 化簡成 \(\frac{1}{m}\) 。「> 0」的判斷留給人——但這裡很簡單,因為 \(\frac{1}{m(m+1)}\) 顯然為正。
2.3.2 Stolz–Cesàro 定理:等價形式與四個應用#
Stolz–Cesàro 定理(又稱 Stolz 定理,1887 年由德國數學家 Johann Stolz 發表,1881 年 Cesàro 獨立給出):
設 \((b_n)\) 是嚴格遞增的實數序列且 \(b_n \to +\infty\) 。若 \(\lim_{n\to\infty}\frac{a_{n+1} - a_n}{b_{n+1} - b_n} = L\) (存在且為有限值),則 \(\lim_{n\to\infty}\frac{a_n}{b_n} = L\) 。
它為什麼重要? 因為它是洛必達法則的離散版本。微積分裡的「 \(\frac{0}{0}\) 」或「 \(\frac{\infty}{\infty}\) 」型極限,在序列版本裡就是「 \(\frac{a_n}{b_n}\) 」型;Stolz–Cesàro 告訴我們:把分子分母都換成差分,會得到同樣的極限。
而且它比洛必達法則更基本:它不需要可導性,只需要可加性。這是「把連續的定理離散化」的典型例子。
先看一個失敗的應用,因為它最能說明「前提」的重要性。 假設我們想用 Stolz–Cesàro 證 \(\lim n\ln\left(1+\frac{1}{n}\right) = 1\) ,取 \(a_n = n\ln\left(1+\frac{1}{n}\right)\) 、 \(b_n = n\) 。SymPy 算出差商 \(\frac{a_{n+1} - a_n}{b_{n+1} - b_n} = a_{n+1} - a_n\) 的極限是 \(0\) ,而不是 \(1\) 。哪裡出錯了?
錯在:我們要的是 \(\lim \frac{a_n}{b_n}\) ,而 \(\frac{a_n}{b_n} = \ln\left(1+\frac{1}{n}\right) \to 0\) ——所以答案 0 其實是對的!我們真正要的量 \(\lim n\ln\left(1+\frac{1}{n}\right)\) 是 \(\frac{a_n}{b_n}\) 這個比值再乘以 \(n\) ,而 \(n\) 正是 \(b_n\) 本身。Stolz–Cesàro 在 \(b_n = n\) 的情況下給不出我們要的資訊——它給的是「比值的極限」,不是「比值乘上發散因子」的極限。
正確的做法是換一組 \(a_n, b_n\) (取倒數型),這正是下面第 1 個應用。定理的前提與「要算的東西對不對得上」同樣重要。
讓我們用它做四件事。
import sympy as sp
m = sp.symbols('m', integer=True, positive=True)
# ---- Stolz-Cesaro 的等價形式 ----
# 定理:若 (b_n) 嚴格遞增且 b_n -> +inf,且 lim (a_{n+1}-a_n)/(b_{n+1}-b_n) = L 存在,
# 則 lim a_n/b_n = L。
# 應用 1:(1+1/n)^n
# 取 a_k = ln(1+1/k), b_k = 1/k(皆遞減,等價於對 -a_k, -b_k 使用遞增型)。
A = sp.log(1 + 1 / m)
B = 1 / m
dA = sp.simplify(A.subs(m, m + 1) - A)
dB = sp.simplify(B.subs(m, m + 1) - B)
print("Stolz-Cesaro 應用 1:證 lim n*ln(1+1/n) = 1")
print(" a_{n+1} - a_n =", dA)
print(" b_{n+1} - b_n =", dB)
ratio = sp.simplify(dA / dB)
print(" 差商 =", ratio)
print(" lim 差商 =", sp.limit(ratio, m, sp.oo))
print(" -> lim n*ln(1+1/n) = 1, 故 lim (1+1/n)^n = exp(1) =", sp.exp(1))
print()
# 應用 2:ln n / n -> 0 (用於證 lim n^(1/n) = 1)
print("Stolz-Cesaro 應用 2:證 lim ln(n)/n = 0")
dA2 = sp.simplify(sp.log(m + 1) - sp.log(m))
dB2 = sp.simplify((m + 1) - m)
print(" a_{n+1} - a_n =", dA2, " b_{n+1} - b_n =", dB2)
print(" lim 差商 =", sp.limit(dA2 / dB2, m, sp.oo))
print(" -> lim ln(n)/n = 0, 故 lim n^(1/n) = exp(0) = 1")
print()
# 應用 3:2^n / n! -> 0
print("Stolz-Cesaro 應用 3:證 lim 2^n/n! = 0")
a3 = 2**m
b3 = sp.factorial(m)
dA3 = sp.simplify(a3.subs(m, m + 1) - a3)
dB3 = sp.simplify(b3.subs(m, m + 1) - b3)
ratio3 = sp.simplify(dA3 / dB3)
print(" a_{n+1} - a_n =", dA3)
print(" b_{n+1} - b_n =", dB3)
print(" 差商 =", ratio3)
print(" lim 差商 =", sp.limit(ratio3, m, sp.oo))
print(" -> lim 2^n/n! = 0")
print()
# 應用 4:H_n / ln n -> 1(Stolz 的招牌例子)
print("Stolz-Cesaro 應用 4:證 lim H_n/ln(n) = 1")
H = sp.harmonic(m)
dA4 = sp.simplify(H.subs(m, m + 1) - H)
dB4 = sp.simplify(sp.log(m + 1) - sp.log(m))
print(" a_{n+1} - a_n =", dA4)
print(" b_{n+1} - b_n =", dB4)
print(" lim 差商 =", sp.limit(dA4 / dB4, m, sp.oo))
實際輸出:
Stolz-Cesaro 應用 1:證 lim n*ln(1+1/n) = 1
a_{n+1} - a_n = log(m*(m + 2)/(m + 1)**2)
b_{n+1} - b_n = -1/(m*(m + 1))
差商 = -log((m*(m + 2)/(m + 1)**2)**(m*(m + 1)))
lim 差商 = 1
-> lim n*ln(1+1/n) = 1, 故 lim (1+1/n)^n = exp(1) = E
Stolz-Cesaro 應用 2:證 lim ln(n)/n = 0
a_{n+1} - a_n = log((m + 1)/m) b_{n+1} - b_n = 1
lim 差商 = 0
-> lim ln(n)/n = 0, 故 lim n^(1/n) = exp(0) = 1
Stolz-Cesaro 應用 3:證 lim 2^n/n! = 0
a_{n+1} - a_n = 2**m
b_{n+1} - b_n = m*factorial(m)
差商 = 2**m/(m*factorial(m))
lim 差商 = 0
-> lim 2^n/n! = 0
Stolz-Cesaro 應用 4:證 lim H_n/ln(n) = 1
a_{n+1} - a_n = -harmonic(m) + harmonic(m + 1)
b_{n+1} - b_n = log((m + 1)/m)
lim 差商 = 1
四個應用的結構完全一致,這正是 Stolz–Cesàro 的威力所在:
| 應用 | \(a_n\) | \(b_n\) | 差商極限 | 結論 |
|---|---|---|---|---|
| 1 | \(\ln(1+\frac{1}{n})\) | \(\frac{1}{n}\) | \(1\) | \(\lim n\ln(1+\frac{1}{n}) = 1 \Rightarrow \lim (1+\frac{1}{n})^n = e\) |
| 2 | \(\ln n\) | \(n\) | \(0\) | \(\lim \frac{\ln n}{n} = 0 \Rightarrow \lim n^{1/n} = 1\) |
| 3 | \(2^n\) | \(n!\) | \(0\) | \(\lim \frac{2^n}{n!} = 0\) |
| 4 | \(H_n\) | \(\ln n\) | \(1\) | \(\lim \frac{H_n}{\ln n} = 1\) |
注意第 1 個應用的巧妙之處: \(b_n = \frac{1}{n}\) 是遞減的,但我們把它取負號就變成遞增了。而 Stolz–Cesàro 的結論是 \(\frac{a_n}{b_n} = \frac{\ln(1+1/n)}{1/n} = n\ln(1+\frac{1}{n})\) ——正是我們要的量。這個技巧叫做「取倒數型」,它把「趨近 1 的數列」的極限問題轉成了「 \(n\) 倍」的極限問題。
注意第 4 個應用是招牌例子: \(\lim \frac{H_n}{\ln n} = 1\) 說明「調和級數的發散速度恰好是對數」。這是第 6 章判別法的基礎。
2.3.3 四個招牌極限:SymPy 的完整驗證#
現在把四個極限放在一起,用 SymPy 做一次「全面體檢」。每一個我們都用三條路徑交叉核對:
- 直接計算(
sp.limit/sp.limit_seq); - 對數化簡(把指數型極限換成乘積型);
- Stolz–Cesàro 或級數展開(定理層級的驗證)。
import sympy as sp
n = sp.symbols('n', integer=True, positive=True)
x = sp.symbols('x', real=True)
print("=== 四個招牌極限的 SymPy 驗證 ===")
targets = [
("lim n*sin(1/n)", n * sp.sin(1 / n), 1),
("lim (1+1/n)^n", (1 + 1 / n)**n, sp.E),
("lim 2^n/n!", 2**n / sp.factorial(n), 0),
("lim n^(1/n)", n**(1 / n), 1),
]
for name, expr, expect in targets:
got = sp.limit(expr, n, sp.oo)
print(f" {name:<22} = {str(got):<8} 期望 {str(expect):<6} 相符 ? {sp.simplify(got - expect) == 0}")
print()
print("=== 對數化簡 ===")
for name, expr in [("(1+1/n)^n", (1 + 1 / n)**n),
("n^(1/n)", n**(1 / n)),
("2^n/n!", 2**n / sp.factorial(n))]:
lg = sp.limit(sp.log(expr), n, sp.oo)
print(f" {name:<14}: ln 後的極限 = {str(lg):<6} -> exp 回來 = {sp.exp(lg)}")
print()
print("=== dir 引數:從哪一邊逼近 ===")
print(" lim(x->0) 1/x dir='+' :", sp.limit(1 / x, x, 0, dir='+'))
print(" lim(x->0) 1/x dir='-' :", sp.limit(1 / x, x, 0, dir='-'))
print(" lim(x->0) 1/x dir='+-':", sp.limit(1 / x, x, 0, dir='+-'))
print(" lim(x->0) |x|/x dir='+' :", sp.limit(sp.Abs(x) / x, x, 0, dir='+'))
print(" lim(x->0) |x|/x dir='-' :", sp.limit(sp.Abs(x) / x, x, 0, dir='-'))
try:
sp.limit(sp.Abs(x) / x, x, 0, dir='+-')
except ValueError as e:
print(" lim(x->0) |x|/x dir='+-': ValueError ->", e)
print()
print("=== sp.Limit:未求值的極限物件 ===")
L = sp.Limit(sp.sin(x) / x, x, 0)
print(" L =", L)
print(" L.doit() =", L.doit())
print(" L 的 args :", L.args)
print()
print("=== function_range:函數在區間上的值域 ===")
from sympy.calculus.util import function_range
for f, iv in [(x**2, sp.Interval(-3, 3)),
(x**2 - 2 * x, sp.Interval(0, 4))]:
print(f" f(x) = {str(f):<16} 在 {str(iv):<18} 的值域 =", function_range(f, x, iv))
print()
print("=== AccumBounds:SymPy 的『誠實失敗』 ===")
A = sp.limit(sp.exp(-x**2) * sp.sin(1 / x), x, 0)
print(" lim(x->0) e^{-x^2} sin(1/x) =", A)
print(" 它的上下界 =", A.args)
B = sp.limit(sp.sin(x) / x, x, 0)
print(" 能算出來時給精確值 :", B, " / 類別 :", type(B).__name__)
print(" 算不出來時給弱結論 :", A, " / 類別 :", type(A).__name__)
實際輸出:
=== 四個招牌極限的 SymPy 驗證 ===
lim n*sin(1/n) = 1 期望 1 相符 ? True
lim (1+1/n)^n = E 期望 E 相符 ? True
lim 2^n/n! = 0 期望 0 相符 ? True
lim n^(1/n) = 1 期望 1 相符 ? True
=== 對數化簡 ===
(1+1/n)^n : ln 後的極限 = 1 -> exp 回來 = E
n^(1/n) : ln 後的極限 = 0 -> exp 回來 = 1
2^n/n! : ln 後的極限 = -oo -> exp 回來 = 0
=== dir 引數:從哪一邊逼近 ===
lim(x->0) 1/x dir='+' : oo
lim(x->0) 1/x dir='-' : -oo
lim(x->0) 1/x dir='+-': zoo
lim(x->0) |x|/x dir='+' : 1
lim(x->0) |x|/x dir='-' : -1
lim(x->0) |x|/x dir='+-': ValueError -> The limit does not exist since left hand limit = -1 and right hand limit = 1
=== sp.Limit:未求值的極限物件 ===
L = Limit(sin(x)/x, x, 0, dir='+')
L.doit() = 1
L 的 args : (sin(x)/x, x, 0, +)
=== function_range:函數在區間上的值域 ===
f(x) = x**2 在 Interval(-3, 3) 的值域 = Interval(0, 9)
f(x) = x**2 - 2*x 在 Interval(0, 4) 的值域 = Interval(-1, 8)
=== AccumBounds:SymPy 的『誠實失敗』 ===
lim(x->0) e^{-x^2} sin(1/x) = AccumBounds(-1, 1)
它的上下界 = (-1, 1)
能算出來時給精確值 : 1 / 類別 : One
算不出來時給弱結論 : AccumBounds(-1, 1) / 類別 : AccumulationBounds
逐項解讀:
(一)四個極限全部「相符 ? True」。 但這還不夠,因為 SymPy 可能用同一個錯誤方法算出四個一致的答案。所以我們用了對數化簡做交叉核對——對數化簡是一條完全不同的計算路徑: \((1+\frac{1}{n})^n\) 的 \(\ln\) 後極限是 \(1\) , \(n^{1/n}\) 是 \(0\) , \(\frac{2^n}{n!}\) 是 \(-\infty\) ;取指數回來分別是 \(E\) 、 \(1\) 、 \(0\) 。
第三個案例特別值得注意: \(\ln\left(\frac{2^n}{n!}\right) = n\ln 2 - \ln(n!) \to -\infty\) 。這揭示了「為什麼 \(\frac{2^n}{n!} \to 0\) 」的真正原因——指數級的增長 \(2^n\) 輸給了階乘級的增長 \(n!\) ,而 \(\ln(n!) \sim n\ln n\) 的增長速度比 \(n\ln 2\) 快。對數化簡把「增長速率的競賽」變成了「加法 vs 加法」的比較。
(二)dir 引數的三種輸出是三種不同的「失敗」。
| 案例 | 輸出 | 意義 |
|---|---|---|
1/x, dir='+' | oo | 右側趨近 \(+\infty\) |
1/x, dir='-' | -oo | 左側趨近 \(-\infty\) |
1/x, dir='+-' | zoo | 雙側的實數極限不存在(zoo = 複數無窮) |
Abs(x)/x, dir='+-' | ValueError | 左右側不一致,明確指出原因 |
ValueError 的訊息特別有價值:
The limit does not exist since left hand limit = -1 and right hand limit = 1
SymPy 不只是說「我不知道」,它說出了「我知道左右各是多少,所以我知道極限不存在」。 這正是我們在 2.1 節稱讚的「誠實失敗」——而且是最有價的一種:它不只說失敗,還說出失敗的結構性原因。
(三)sp.Limit 與 sp.limit 的分工。 sp.Limit(...) 建立一個物件,你可以印它、查它的 .args、呼叫 .doit()。注意 L 的 args : (sin(x)/x, x, 0, +)——連「從哪一邊逼近」這個資訊都存進物件裡。
(四)sp.calculus.util.function_range 讓我們能思考「值域」問題。 很多夾擠定理的敘述都是「 \(f\) 在 \([a,b]\) 上的值域被夾在某處」。例如 \(x^2 - 2x\) 在 \([0, 4]\) 上的值域是 \([-1, 8]\) ——這就是它的最小值與最大值(對應極值定理)。
(五)AccumBounds 的兩種失敗形態。 對比 B(類別 One,精確值)與 A(類別 AccumulationBounds,弱界),我們看到 SymPy 的三級誠實度:算出來了 → 給精確值(1);算不出來但有界 → 給弱界(AccumBounds(-1, 1));左右不一致 → 丟 ValueError 並說明原因。
2.3.4 用數值與隨機取樣尋找反例#
最後,也是本節最重要的一節:SymPy 不能取代定義,但能加速反例搜尋。
讓我們把這個工作流完整地跑一遍。我們要做的不是「證明 \(\lim_{x\to0}\frac{\sin x}{x} = 1\) 」(那需要 \(\varepsilon\) - \(\delta\) ),而是:
- 嘗試推翻它(隨機取樣);
- 測量誤差的行為(對數刻度取樣 + 雙對數圖);
- 檢驗一個更強的候選命題( \(|\frac{\sin x}{x} - 1| < \frac{x^2}{6}\) 對所有 \(x > 0\) );
- 用取樣估計極限值,並與猜的係數比對(找到錯誤的猜測);
- 用 SymPy 的級數揭穿真相。
import numpy as np
import sympy as sp
import matplotlib.pyplot as plt
# ---- 用數值與隨機取樣尋找反例的工作流 ----
# 定理候選:「lim(x->0) sin(x)/x = 1」
# 我們不去證明它,而是先努力把它『推翻』:隨機取樣,看有沒有反例。
rng = np.random.default_rng(115)
x = sp.symbols('x', real=True)
# (1) 隨機取樣:在 (0, 1) 上撒 2000 個點,看 |f(x)-1| 會不會超過某個門檻
xs = rng.uniform(1e-6, 1.0, 2000)
errs = np.array([abs(np.sin(xv) / xv - 1.0) for xv in xs])
order = np.argsort(xs)
half = len(xs) // 2
print("(1) 隨機取樣尋找反例:lim(x->0) sin(x)/x = 1")
print(" 取樣點數 :", len(xs))
print(" 最大誤差 :", errs.max(), " 對應的 x =", xs[np.argmax(errs)])
print(" 最小 x :", xs.min(), "(最靠近 0 的取樣點)")
print(" 最小一半 x 的最大誤差 :", errs[order[:half]].max())
print(" 較大一半 x 的最大誤差 :", errs[order[half:]].max())
print(" -> 靠近 0 的一批誤差遠小於遠離 0 的一批,方向正確")
print()
# (2) 更聰明的取樣:對 x 用對數刻度,才能逼近 0
logxs = np.logspace(-12, 0, 6)
print("(2) 對數刻度取樣(逼近 0 的正確方式)")
for xv in logxs:
print(f" x = {xv:>10.3e} sin(x)/x - 1 = {np.sin(xv)/xv - 1:+.6e} x^2/6 = {xv**2/6:.6e}")
print(" -> 誤差與 x^2/6 同階,這預告了誤差是 O(x^2)")
print()
# (3) 用 SymPy 反過來驗證這個預測:級數展開
print("(3) SymPy 級數展開確認誤差量級")
print(" sin(x)/x - 1 =", sp.series(sp.sin(x) / x - 1, x, 0, 7))
print()
# (4) 估計極限值,並與猜的係數比對:候選命題是錯的
print("(4) 用取樣估計極限值,並與猜的係數比對")
print(" 候選命題:lim(x->0) (e^x - 1 - x)/x^2 = 1/6 (猜錯了)")
xs3 = rng.uniform(1e-4, 1e-3, 500)
vals = np.array([(np.exp(xv) - 1 - xv) / xv**2 for xv in xs3])
print(" 隨機取樣估計出的極限值 ≈", round(float(vals.mean()), 6), " (而不是 1/6 =", 1/6, ")")
print(" -> 差異很大,說明候選命題是假的")
print(" SymPy 級數揭穿真相:")
print(" ", sp.series((sp.exp(x) - 1 - x) / x**2, x, 0, 5))
print(" SymPy 直接算出的極限 =", sp.limit((sp.exp(x) - 1 - x) / x**2, x, 0))
print()
# (5) 找到一個明確的反例:把『對所有 x>0 成立』的強命題推翻
print("(5) 推翻強命題:『|sin(x)| <= x^2 對所有 x > 0 成立』")
viol_x = None
for _ in range(2000):
xv = rng.uniform(1e-6, 2.0)
if abs(np.sin(xv)) > xv**2:
viol_x = xv
break
print(" 隨機取樣找到的反例 x =", viol_x)
print(" 精確驗證:取 x = 1/2")
print(" |sin(1/2)| =", sp.Abs(sp.sin(sp.Rational(1, 2))))
print(" (1/2)^2 =", sp.Rational(1, 4))
print(" 反例成立 ?", sp.Abs(sp.sin(sp.Rational(1, 2))) > sp.Rational(1, 4))
print(" -> 反例的真正用處不是推翻結論,而是提醒我們:")
print(" 定理總要帶上『適用範圍』,而找出那個範圍正是嚴格化的意義")
print()
# (6) 把取樣結果畫出來:雙對數座標確認誤差量級
fig, ax = plt.subplots(figsize=(7, 4.5))
xs4 = np.logspace(-10, -1, 400)
ax.loglog(xs4, np.abs(np.sin(xs4) / xs4 - 1), "b-", label="|sin(x)/x - 1|")
ax.loglog(xs4, xs4**2 / 6, "k--", label="x^2 / 6")
ax.loglog(xs4, xs4**2 / 7, "k:", label="x^2 / 7")
ax.set_xlabel("x")
ax.set_ylabel("error")
ax.set_title("Error of sin(x)/x near 0 (log-log scale)")
ax.legend()
plt.tight_layout()
plt.savefig("/tmp/sin_over_x_error.png", dpi=120)
print(" 已存 /tmp/sin_over_x_error.png")
print(" 兩條參考線夾住實測曲線 -> 誤差確實是 Theta(x^2)")
實際輸出:
(1) 隨機取樣尋找反例:lim(x->0) sin(x)/x = 1
取樣點數 : 2000
最大誤差 : 0.1578013418560268 對應的 x = 0.9975815110891472
最小 x : 1.5983754111375778e-05 (最靠近 0 的取樣點)
最小一半 x 的最大誤差 : 0.04178966839166842
較大一半 x 的最大誤差 : 0.1578013418560268
-> 靠近 0 的一批誤差遠小於遠離 0 的一批,方向正確
(2) 對數刻度取樣(逼近 0 的正確方式)
x = 1.000e-12 sin(x)/x - 1 = +0.000000e+00 x^2/6 = 1.666667e-25
x = 2.512e-10 sin(x)/x - 1 = +0.000000e+00 x^2/6 = 1.051596e-20
x = 6.310e-08 sin(x)/x - 1 = -6.661338e-16 x^2/6 = 6.635120e-16
x = 1.585e-05 sin(x)/x - 1 = -4.186473e-11 x^2/6 = 4.186477e-11
x = 3.981e-03 sin(x)/x - 1 = -2.641487e-06 x^2/6 = 2.641489e-06
x = 1.000e+00 sin(x)/x - 1 = -1.585290e-01 x^2/6 = 1.666667e-01
-> 誤差與 x^2/6 同階,這預告了誤差是 O(x^2)
(3) SymPy 級數展開確認誤差量級
sin(x)/x - 1 = -x**2/6 + x**4/120 - x**6/5040 + O(x**7)
(4) 用取樣估計極限值,並與猜的係數比對
候選命題:lim(x->0) (e^x - 1 - x)/x^2 = 1/6 (猜錯了)
隨機取樣估計出的極限值 ≈ 0.50009 (而不是 1/6 = 0.16666666666666666 )
-> 差異很大,說明候選命題是假的
SymPy 級數揭穿真相:
1/2 + x/6 + x**2/24 + x**3/120 + x**4/720 + O(x**5)
SymPy 直接算出的極限 = 1/2
(5) 推翻強命題:『|sin(x)| <= x^2 對所有 x > 0 成立』
隨機取樣找到的反例 x = 0.8393914180193854
精確驗證:取 x = 1/2
|sin(1/2)| = sin(1/2)
(1/2)^2 = 1/4
反例成立 ? True
-> 反例的真正用處不是推翻結論,而是提醒我們:
定理總要帶上『適用範圍』,而找出那個範圍正是嚴格化的意義
已存 /tmp/sin_over_x_error.png
兩條參考線夾住實測曲線 -> 誤差確實是 Theta(x^2)
flowchart TD
A["柯西 1821<br/>eps-delta 定義<br/>連續性 = lim f(x) = f(a)"] --> B["Weierstrass 1872<br/>分析 = 不等式的演算術<br/>delta 要能被構造"]
B --> C["Dedekind 1872<br/>切割 = 實數的構造<br/>完成化:柯西數列必有極限"]
B --> D["Heine 1824<br/>有限覆蓋 = 把無窮變成有限"]
D --> E["Bolzano-Weierstrass<br/>有界數列必有收斂子列"]
A --> F["SymPy: sp.limit 先猜再驗證<br/>sp.Limit 未求值 / AccumBounds 弱界<br/>ValueError 說明失敗原因"]
B --> G["SymPy: sp.solve_univariate_inequality<br/>把 eps-delta 變成可操作的區間"]
C --> H["SymPy: Fraction + sp.floor<br/>用有理數重建切割"]
E --> I["SymPy + numpy<br/>隨機取樣尋找反例<br/>用數值加速懷疑,用符號確認"]
F --> J["2.3 節:夾擠 / 單調有界 / Stolz-Cesaro<br/>四個招牌極限的三路交叉驗證"]
G --> J
H --> J
I --> J
J --> K["第 3 章:導數 = 差商的極限<br/>用定義重算微分"]習題#
習題 2.3.1(夾擠定理的完整實作) (a) 用 SymPy 證明 \(\lim_{x\to 0}\frac{1-\cos x}{x^2} = \frac{1}{2}\) :找出一個夾擠(提示: \(1 - \cos x = 2\sin^2\frac{x}{2}\) ,然後用 \(\sin t \le t\) )。 (b) 用 SymPy 證明 \(\lim_{x\to 0}\frac{\arctan x}{x} = 1\) (提示: \(x = \tan(\arctan x)\) ,把問題轉成 \(\lim \frac{\tan t}{t} = 1\) )。 (c) 用夾擠定理證明 \(\lim_{x \to +\infty}\left(1 + \frac{1}{x}\right)^x = e\) 的上界是 \(e\) 、下界是 \(1\) 。這裡「上界 \(e\) 」本身需要一個獨立的證明——請說明為什麼這是「循環」的,以及如何用對數化簡打破循環。 (d) 用 sp.limit 驗證 (a)、(b) 的結論,並解釋為什麼 SymPy 在這裡能成功而 \(\lim_{x\to 0}e^{-x^2}\sin\frac{1}{x}\) 卻失敗(提示:比較兩者需要處理的高頻振盪)。
習題 2.3.2(單調有界定理的應用) (a) 證明數列 \(a_n = \left(1 + \frac{1}{n}\right)^n\) 遞增且以 \(e\) 為上界,因此收斂。(提示:先用二項式定理比較 \(a_{n+1}\) 與 \(a_n\) 。) (b) 證明數列 \(b_n = \left(1 - \frac{1}{n}\right)^n\) ( \(n \ge 2\) )遞減且以 \(0\) 為下界,因此收斂,且極限是 \(\frac{1}{e}\) 。 (c) 用 SymPy 對 (a)、(b) 做「差分的符號」檢查,並用 sp.limit_seq 核對極限值。 (d) 說明:為什麼「單調 + 有界」兩個條件缺一不可?分別舉一個反例(用 sp.limit_seq 展示它不收斂)。
習題 2.3.3(Stolz–Cesàro 的其他應用) 用 Stolz–Cesàro 定理證明:(a) \(\lim_{n\to\infty} \frac{n!}{n^n} = 0\) ; (b) \(\lim_{n\to\infty} \sqrt[n]{n!} = +\infty\) ; (c) \(\lim_{n\to\infty} \frac{1 + 2 + \cdots + n}{n^2} = \frac{1}{2}\) ; (d) \(\lim_{n\to\infty} \frac{n^2}{1^2 + 2^2 + \cdots + n^2} = \frac{3}{2}\) 。 對每一題,先寫出 \(a_n\) 、 \(b_n\) 的選擇,再用 SymPy 計算差商並核對極限。特別注意 (a): \(b_n = n^n\) 時 \(b_{n+1} - b_n\) 很難化簡——說明你會怎麼處理這個困難。
習題 2.3.4(SymPy 的三種誠實失敗) (a) 構造三個案例,使得 sp.limit 分別回傳:(i) 未求值的 Limit 物件、(ii) AccumBounds、(iii) 丟出 ValueError。對每一個,說明 SymPy「知道什麼」與「不知道什麼」。 (b) sp.limit(sp.exp(-x**2) * sp.sin(1/x), x, 0) 回傳 AccumBounds(-1, 1)。你自己證明真實的極限是 \(0\) (用 \(|e^{-x^2}| \le 1\) 與 \(\lvert \sin \frac{1}{x} \rvert \le 1\) ),並解釋 SymPy 為什麼做不到。提示:SymPy 的 Gruntz 演算法要處理「高頻振盪」,而 \(e^{-x^2}\) 的衰減速度與 \(\sin\frac{1}{x}\) 的振盪速度的比值關係是關鍵。 (c) 用 sp.Limit 建立上面那個極限的物件,印出它,呼叫 .doit(),並說明 Limit 與 limit 的關係(提示:一個是「物件」,一個是「求值」)。
習題 2.3.5(反例搜尋工作流) (a) 對候選命題「 \(\lim_{n \to \infty} \frac{n^2 + 1}{n^2 + n} = 1\) 」做反例搜尋:隨機取樣 10000 個 \(n\) ,計算誤差,找出最壞情況。結論是什麼? (b) 對候選命題「 \(\lim_{n\to\infty}\left(1 + \frac{1}{n}\right)^n\) 從上方逼近 \(e\) 」做反例搜尋:隨機取樣並找出反例(若有)。然後用 SymPy 證明真相(提示: \(\left(1+\frac{1}{n}\right)^n < e\) 是經典不等式,可用 \(\ln(1+t) < t\) 證明)。 (c) 找一個「SymPy 算得出來但結論是錯的」例子:也就是一個 SymPy 因為缺少假設而給出錯誤答案的案例。提示:嘗試 sp.limit((1 + 1/x)**x, x, sp.oo)(注意 \(x\) 的符號假設),並解釋錯誤的來源。 (d) 綜合 (a)–(c),寫出你自己的一套「極限定理驗證工作流」,說明每一步用 SymPy 做什麼、用人力做什麼。
小結#
- 夾擠定理把困難的極限夾在兩個容易的極限之間。SymPy 的角色:算出兩端極限、用級數展開驗證不等式的方向、用精確有理數抽查。
- 單調有界定理要求兩個條件:單調 + 有界。SymPy 負責把 \(a_{n+1} - a_n\) 與「上界 \(- a_n\) 」化簡成可判符號的式子,人負責「> 0」的推理。
- Stolz–Cesàro 定理是洛必達法則的離散版本: \(\lim \frac{a_n}{b_n} = \lim \frac{a_{n+1} - a_n}{b_{n+1} - b_n}\) (當 \((b_n)\) 嚴格遞增且 \(\to +\infty\) )。它只需要可加性,不需要可導性。它的前提很重要:用錯 \(b_n\) 會得到無關的結論。
- 四個招牌極限: \(\lim n\sin\frac{1}{n} = 1\) 、 \(\lim\left(1+\frac{1}{n}\right)^n = e\) 、 \(\lim\frac{2^n}{n!} = 0\) 、 \(\lim\sqrt[n]{n} = 1\) 。用三條路徑(直接、對數化簡、Stolz–Cesàro/級數)交叉核對,才能排除「SymPy 用同一個錯誤方法算出四個一致答案」的可能。
- 對數化簡是處理指數型極限的通用武器:把 \(\lim a(x)^{b(x)}\) 換成 \(\lim b(x)\ln a(x)\) 。它也是「增長速率競賽」的翻譯器( \(\frac{2^n}{n!} \to 0\) 的本質是 \(\ln n! \sim n\ln n\) 贏過 \(n\ln 2\) )。
- SymPy 的三種誠實失敗:
sp.Limit(未求值物件)、AccumBounds(弱界,例如 \(\lim_{x\to0}e^{-x^2}\sin\frac{1}{x}\) 給出AccumBounds(-1, 1)而真值是 \(0\) )、ValueError(左右不一致,並說明左右各是多少)。第三種最有價值:它不只說「失敗」,還說出失敗的結構性原因。 sp.calculus.util.function_range讓我們能思考「值域」,這在寫夾擠敘述與極值定理時非常有用。- 反例搜尋工作流:隨機取樣(定性)→ 對數刻度取樣(定量)→ 級數展開(確認量級)→ 找出猜錯的係數 → 用精確有理數做最終核對。反例的真正價值不是推翻定理,而是找出定理的「適用範圍」——而那正是嚴格化的意義。
SymPy 不能取代定義,但能加速反例搜尋與機械運算。
第 2 章到這裡結束了。我們已經把「極限」從一個直覺變成了一門可以逐條檢查的語言,並且看到了機器如何(以及如何不能)參與這門語言的工作。下一章(第 3 章)我們要處理導數:費馬的極值法與切線問題、萊布尼茲的 \(\frac{\mathrm{d}y}{\mathrm{d}x}\) 記號,然後用 SymPy 從極限定義重新推出導數表。那將是第 2 章成果的一次全面回收——因為導數的定義本身就是一個極限。
本節 SymPy 新工具清單#
| 工具 | 用途 | 例 |
|---|---|---|
sp.limit_seq(expr, n) | 數列極限 | sp.limit_seq(1 - 1/n, n) |
sp.simplify(a.subs(n, n+1) - a) | 差分(Stolz–Cesàro、單調性) | sp.simplify(a_m.subs(m, m+1) - a_m) |
sp.harmonic(n) | 調和數 | sp.harmonic(m) |
sp.log / sp.exp | 對數化簡 | sp.limit(sp.log(expr), n, sp.oo) |
sp.Limit(expr, z, z0) / .doit() / .args | 未求值極限物件 | sp.Limit(sp.sin(x)/x, x, 0) |
sp.limit(..., dir='+'/'-'/'+-') | 單側/雙側 | sp.limit(1/x, x, 0, dir='+-') |
sp.calculus.util.function_range | 區間上的值域 | function_range(x**2, x, sp.Interval(-3,3)) |
sp.AccumBounds / AccumulationBounds | 弱界結論 | sp.limit(sp.exp(-x**2)*sp.sin(1/x), x, 0) |
sp.series(expr, x, 0, k) | 誤差量級與主項 | sp.series((sp.exp(x)-1-x)/x**2, x, 0, 5) |
np.logspace / np.allclose / np.ptp | 對數刻度取樣與收斂檢查 | np.logspace(-12, 0, 15) |
sp.solve_univariate_inequality | 把不等式變成區間 | sp.solve_univariate_inequality(y**2 < eps, y) |