跳轉到

第 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 年提出),它的邏輯大致是:

  1. 嘗試把極限化簡成已知的基本極限(例如 \(\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\) );
  2. 化簡的手段包括泰勒展開、洛必達法則(對 \(0/0\) 與 \(\infty/\infty\) 型)、極限的乘除與冪次法則;
  3. 若化簡無法收斂,則回報失敗——留下未求值的 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\left(a(x)^{b(x)}\right) = b(x)\log a(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

「對數化簡」的完整邏輯鏈在這裡一目了然:

  1. sp.limit(a, n, sp.oo) 直接给出 E。
  2. 但我們不滿足於這個黑箱。取對數:log((1+1/n)^n) = n*log(1 + 1/n)(sp.expand_log 做的正是這個代數化簡)。
  3. sp.limit(sp.log(a), n, sp.oo) 給出 1——這個極限容易得多,因為 \(\log(1 + t) \sim t\) ,所以 \(n\log\left(1 + \frac{1}{n}\right) \to 1\) 。
  4. 取指數回來: \(e^1 = e\) 。sp.limit(sp.log(a), n, sp.oo) == 1 回傳 True,確認了這一步的精確性。
  5. 最後用級數估計誤差: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\) 有

\[ 0 \le x - \sin x \le \frac{x^3}{6} \]

於是

\[ \left|\frac{\sin x}{x} - 1\right| = \frac{|x - \sin x|}{x} \le \frac{x^2}{6} \]

取 \(\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) = \frac{x^3}{6} - x + \sin x \]

則 \(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