4.1 函數即物件#
本節目標#
- 理解數學函數 \(f(x)\) 與 Python 函數的異同
- 會用
Lambda建立符號函數,用subs代入求值 - 會用
lambdify把符號運算式轉成快速的 NumPy 函數(效能對照) - 能進行函數合成 \(f(g(x))\)
- 動手實驗:建立 \(f(x) = x^2 - 4x + 3\) 並製表求值
數學函數 vs Python 函數#
數學的 \(f(x) = x^2 - 4x + 3\) 是一條規則:給定輸入就決定輸出。Python 有兩種表示方式:
def f(x): # 方式一:純 Python 函數(只能算數值)
return x**2 - 4*x + 3
print(f(1)) # 0
import sympy as sp
x = sp.Symbol('x')
expr = x**2 - 4*x + 3 # 方式二:符號運算式(能思考)
print(sp.factor(expr)) # (x - 1)*(x - 3)
print(sp.diff(expr, x)) # 2*x - 4
| Python 函數 | SymPy 符號運算式 | |
|---|---|---|
| 輸入 | 只能是數值 | 符號、數值、其他運算式皆可 |
| 能力 | 只能計算 | 化簡、微分、解方程 |
| 速度 | 快 | 慢 |
本書策略:用符號運算式做數學思考,用 lambdify 轉成 Python 函數做大量數值計算。
Lambda:匿名函數#
Lambda(變數, 運算式) 對應數學記法 \(\lambda x.\, x^2 - 4x + 3\) ,是「等著被代入的規則」:
f = sp.Lambda(x, x**2 - 4*x + 3)
print(f(5)) # 8
print(f(sp.sin(x))) # 代入運算式也可以
subs:代入求值#
expr = x**2 - 4*x + 3
print(expr.subs(x, 2)) # -1
print(expr.subs(x, sp.Rational(1, 2))) # 5/4(精確分數)
print(sp.expand(expr.subs(x, x + 1))) # x**2 - 2*x ← 這就是 f(x+1)
subs(x, x+1) 把每個 \(x\) 換成 \((x+1)\) ,正是計算 \(f(x+1)\) ;多變數用字典:e.subs({x: 1, y: 2})。求浮點值用 sp.N(..., 10)。
lambdify:從符號到數值的橋樑#
符號運算聰明但慢;要代入大量點(畫圖、數值積分)就用 lambdify 轉成 NumPy 函數:
import numpy as np
expr = sp.sin(x) * sp.exp(-x/10) + x**3
f = sp.lambdify(x, expr, 'numpy') # 向量化後端
print(f(np.linspace(0, 10, 5))) # 一次算整個陣列
效能對照實驗#
import sympy as sp, numpy as np, time
x = sp.Symbol('x')
expr = sp.sin(x) * sp.exp(-x/10) + x**3
N = 100000
xs_list = [i * 0.001 for i in range(N)]
xs_np = np.linspace(0, N * 0.001, N)
t0 = time.time() # 方法 1:符號 subs(只跑 2000 次再外推)
for v in xs_list[:2000]:
expr.subs(x, v).evalf()
subs_time = (time.time() - t0) / 2000 * N
f_py = sp.lambdify(x, expr, 'math') # 方法 2:lambdify 逐點
t0 = time.time()
for v in xs_list:
f_py(v)
loop_time = time.time() - t0
f_np = sp.lambdify(x, expr, 'numpy') # 方法 3:向量化
t0 = time.time()
f_np(xs_np)
vec_time = time.time() - t0
print(f"符號 subs 外推:{subs_time:.3f} 秒")
print(f"lambdify 迴圈 :{loop_time:.3f} 秒")
print(f"向量化 :{vec_time:.4f} 秒")
典型結果:符號 subs 慢數萬倍。結論:符號推導 → lambdify → NumPy 大量計算。多變數:sp.lambdify((x, y), x**2 + y**2, 'numpy')。
函數合成#
\((f \circ g)(x) = f(g(x))\) 在 SymPy 中就是 subs:
f = x**2 + 1
g = sp.sin(x)
h = f.subs(x, g) # sin(x)**2 + 1
print(h.subs(x, sp.pi/6)) # 5/4
k = g.subs(x, f) # g(f(x)) = sin(x**2 + 1)
注意 \(f(g(x)) \neq g(f(x))\) ,合成不可交換——用程式驗證比背誦更有感覺。
動手實驗#
建立 \(f(x) = x^2 - 4x + 3\) 並製表求值
import sympy as sp
import numpy as np
x = sp.Symbol('x')
f_expr = x**2 - 4*x + 3
print("因式分解:", sp.factor(f_expr)) # (x - 1)*(x - 3)
print(f"{'x':>6} | {'f(x)':>8}")
print("-" * 18)
for v in [0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4]:
print(f"{v:>6} | {float(f_expr.subs(x, v)):>8.2f}")
f = sp.lambdify(x, f_expr, 'numpy') # lambdify 版:一次算整排
xs = np.arange(0, 4.5, 0.5)
print("numpy 版:", f(xs))
assert abs(f(1.0)) < 1e-12 and abs(f(3.0)) < 1e-12
print("根驗證通過:f(1) = f(3) = 0")
進一步挑戰:把 \(f\) 改成 \(x^3 - 3x + 1\) 觀察變號位置;寫 table(expr, a, b, n) 自動製表;用 subs 算 \(f(f(2))\) 並手算驗證。
小結與練習#
- Python 函數「只能算」,SymPy 符號運算式「能思考」
Lambda建立符號函數;subs是代入與合成的核心lambdify(..., 'numpy')通往高速數值計算,向量化快上萬倍- 函數合成 \(f(g(x))\) 就是
f.subs(x, g),順序不可交換
練習:
- 定義 \(g(t) = e^{-t}\sin(2t)\) ,求 \(g(\pi/4)\) 的精確值與 10 位近似。
- 用
lambdify求 \(g(t)\) 在一百萬個取樣點上的平均值。 - 驗證 \(x^2\) 與 \(x+3\) 的兩種合成之差為 \(6x + 9\) 恆不為零。
- 寫
compose_many(exprs)依序合成,如[x+1, x**2, sp.sin(x)]得 \(\sin((x+1)^2)\) 。