跳轉到

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),順序不可交換

練習:

  1. 定義 \(g(t) = e^{-t}\sin(2t)\) ,求 \(g(\pi/4)\) 的精確值與 10 位近似。
  2. 用 lambdify 求 \(g(t)\) 在一百萬個取樣點上的平均值。
  3. 驗證 \(x^2\) 與 \(x+3\) 的兩種合成之差為 \(6x + 9\) 恆不為零。
  4. 寫 compose_many(exprs) 依序合成,如 [x+1, x**2, sp.sin(x)] 得 \(\sin((x+1)^2)\) 。