跳轉到

第 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):若

\[ g(x) \le f(x) \le h(x) \qquad (0 < |x-a| < \delta), \]

且 \(\lim_{x \to a} g(x) = \lim_{x \to a} h(x) = L\) ,則 \(\lim_{x \to a} f(x) = L\) 。

它的價值在於「把困難的極限夾在兩個容易的極限之間」。 最著名的應用是

\[ \lim_{x \to 0}\frac{\sin x}{x} = 1, \]

因為對 \(0 < x < \frac{\pi}{2}\) 有 \(\cos x \le \frac{\sin x}{x} \le 1\) ,而 \(\cos x \to 1\) 。

SymPy 能做什麼? 三件事:

  1. 算出兩端的極限(確認它們真的相等);
  2. 用級數展開驗證不等式(把「 \(\cos x \le \frac{\sin x}{x} \le 1\) 」這個需要人做的推理,變成「兩個差函數的級數主項都為正」);
  3. 用精確有理數做抽查。
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 做一次「全面體檢」。每一個我們都用三條路徑交叉核對:

  1. 直接計算(sp.limit / sp.limit_seq);
  2. 對數化簡(把指數型極限換成乘積型);
  3. 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\) ),而是:

  1. 嘗試推翻它(隨機取樣);
  2. 測量誤差的行為(對數刻度取樣 + 雙對數圖);
  3. 檢驗一個更強的候選命題( \(|\frac{\sin x}{x} - 1| < \frac{x^2}{6}\) 對所有 \(x > 0\) );
  4. 用取樣估計極限值,並與猜的係數比對(找到錯誤的猜測);
  5. 用 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)


習題#

習題 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)