Python 自訂函數(def)讓你把一段可重複使用的程式碼封裝成有名字的單元,呼叫時只需一行,大幅減少重複程式碼、提升可讀性與維護性。本文完整解析 def 的定義語法、位置參數與預設值參數的設計、return 回傳值的運作機制、區域變數與全域變數的作用域規則,以及如何設計出乾淨、職責單一的函數,附量化策略指標計算函數實戰範例。
什麼是自訂函數?
自訂函數是用 def 關鍵字定義的可重複呼叫的程式碼封裝單元,把「做某件事的一組指令」包裝成有名字的工具,需要時呼叫名字即可執行,不需要把相同程式碼複製貼上。函數可以接收輸入(參數),執行處理後透過 return 回傳輸出(回傳值)。
可以把自訂函數想像成煉金坊的專屬配方模具:你把原料(參數)投入,模具按照固定流程提煉,最後輸出精煉品(回傳值)。每次需要同樣的煉製結果,只要呼叫模具名稱,不需要重新描述整個流程。這正是「Don't Repeat Yourself(DRY)」原則的核心體現。
核心語法:def、參數與 return
# === 基本函數定義語法 ===
# 無參數、無回傳值的函數
def greet():
print("歡迎來到離火煉金坊!")
greet() # 呼叫函數:歡迎來到離火煉金坊!
greet() # 可以重複呼叫:歡迎來到離火煉金坊!
# 有參數的函數(參數是函數的輸入)
def greet_user(name): # name 是參數(形式參數)
print(f"你好,{name}!")
greet_user("煉金師") # 傳入引數:你好,煉金師!
greet_user("Python") # 你好,Python!
函數定義語法:def 函數名稱(參數列表):,冒號後的縮排區塊是函數本體。函數必須先定義後呼叫,若在def之前就呼叫會觸發NameError。函數名稱命名慣例與變數相同:全小寫,多字詞用底線連接(snake_case)。
# === return:回傳值 ===
# 沒有 return 的函數,隱式回傳 None
def add_no_return(a, b):
result = a + b
# 沒有 return,呼叫後得到 None
val = add_no_return(3, 5)
print(val) # None
# 有 return 的函數,回傳計算結果
def add(a, b):
return a + b # 計算完畢,回傳結果給呼叫者
result = add(3, 5) # result 接住回傳值
print(result) # 8
# return 可以回傳任何型別
def get_info(ticker):
return f"代碼:{ticker}" # 回傳字串
def is_valid_price(price):
return price > 0 # 回傳布林值
def get_stats(data):
return min(data), max(data), sum(data) / len(data) # 回傳多個值(Tuple)
low, high, avg = get_stats([100, 105, 98, 112, 95])
print(f"低:{low} 高:{high} 均:{avg:.1f}")
# 低:95 高:112 均:102.0
return有兩個作用:回傳值給呼叫者,以及立即結束函數執行。return後面的程式碼不會執行。回傳多個值時,Python 自動包成 Tuple,可用多重賦值解包接收。
規則與注意事項
| 規則 | 正確範例 | 錯誤範例 / 陷阱 |
|---|---|---|
| 函數必須先定義後呼叫 | def 寫在呼叫之前 | 呼叫在 def 之前 → NameError |
| 無 return 的函數回傳 None | 明確寫 return 值 才有回傳 |
誤以為 print() 等同 return,用變數接住得到 None |
| 函數內的變數是區域變數,外部不可見 | 透過 return 把值傳出去 | 直接在外部使用函數內部變數 → NameError |
| 參數數量要與定義一致 | add(3, 5)(def add(a, b)) |
add(3) → TypeError: missing argument |
| 函數應職責單一,只做一件事 | 計算函數只計算,不印出結果 | 一個函數又計算、又印出、又存檔 → 難以測試維護 |
常見錯誤與防呆
錯誤一:誤以為 print() 等同 return,用變數接住得到 None
# ❌ 錯誤寫法(函數內只有 print,沒有 return)
def multiply(a, b):
print(a * b) # 只印出,不回傳
result = multiply(4, 5) # 印出:20
print(result) # None!因為函數沒有 return
# ✅ 正確寫法:用 return 回傳結果
def multiply(a, b):
return a * b
result = multiply(4, 5)
print(result) # 20
錯誤二:在函數外部存取函數內的區域變數
# ❌ 錯誤寫法(函數內的 total 是區域變數,外部不可見)
def calc_total(price, qty):
total = price * qty # total 只存在函數內部
calc_total(100, 5)
print(total) # NameError: name 'total' is not defined
# ✅ 正確做法:用 return 把結果傳出
def calc_total(price, qty):
total = price * qty
return total
result = calc_total(100, 5)
print(result) # 500
錯誤三:在呼叫之後才定義函數
# ❌ 錯誤寫法(呼叫在定義之前)
result = add(3, 5) # NameError: name 'add' is not defined
def add(a, b):
return a + b
# ✅ 正確寫法:定義永遠在呼叫之前
def add(a, b):
return a + b
result = add(3, 5)
print(result) # 8
進階用法
預設值參數(Default Parameters)
函數的參數可以設定預設值,呼叫時若沒有傳入該參數,就使用預設值,讓函數在不同情境下都能靈活使用而不需要每次都傳齊所有引數。
# 預設值參數:參數 = 預設值
def calc_profit(buy, sell, shares=100, fee_rate=0.001425):
gross = (sell - buy) * shares
fee = (buy + sell) * shares * fee_rate
return round(gross - fee, 2)
# 只傳必要參數,其餘使用預設值
print(calc_profit(100, 115)) # 使用預設 shares=100, fee_rate=0.001425
# 1500 - (100+115)*100*0.001425 ≈ 1469.39
# 覆蓋預設值
print(calc_profit(100, 115, shares=500)) # 改為 500 股
print(calc_profit(100, 115, 200, 0.003)) # 全部自訂
# 規則:有預設值的參數必須放在無預設值參數之後
# ❌ def bad(a=1, b): → SyntaxError
# ✅ def good(a, b=1): → 正確
作用域:區域變數與全域變數
函數內定義的變數是區域變數(Local Variable),只在函數內部存在;函數外定義的是全域變數(Global Variable),函數內可以讀取但不能直接修改(除非使用 global 關鍵字,但不建議)。
tax_rate = 0.3 # 全域變數
def calc_after_tax(income):
tax = income * tax_rate # 函數內可以讀取全域變數
net = income - tax # net 是區域變數
return net
print(calc_after_tax(100000)) # 70000.0
# print(net) # NameError:net 只存在函數內部
# 若需要在函數內修改全域變數(不推薦,盡量避免)
counter = 0
def increment():
global counter # 宣告要修改全域變數
counter += 1
increment()
increment()
print(counter) # 2
# ✅ 更好的做法:透過 return 回傳新值,不直接修改全域變數
def increment_val(val):
return val + 1
counter = increment_val(counter)
counter = increment_val(counter)
print(counter) # 4
量化實戰:技術指標計算函數庫
將常用指標封裝成函數,讓回測程式碼更模組化、可重複使用,是量化開發的基本架構原則。
def calc_sma(prices, period):
"""計算簡單移動平均線(SMA)"""
if len(prices) < period:
return None
return round(sum(prices[-period:]) / period, 4)
def calc_rsi(prices, period=14):
"""計算 RSI 相對強弱指數"""
if len(prices) < period + 1:
return None
changes = [prices[i] - prices[i-1] for i in range(1, len(prices))]
gains = [c for c in changes[-period:] if c > 0]
losses = [abs(c) for c in changes[-period:] if c < 0]
avg_gain = sum(gains) / period if gains else 0
avg_loss = sum(losses) / period if losses else 0
if avg_loss == 0:
return 100.0
rs = avg_gain / avg_loss
return round(100 - (100 / (1 + rs)), 2)
def is_buy_signal(prices, sma_period=20, rsi_period=14, rsi_threshold=35):
"""判斷是否觸發買入信號:收盤在均線上方且 RSI 超賣"""
sma = calc_sma(prices, sma_period)
rsi = calc_rsi(prices, rsi_period)
if sma is None or rsi is None:
return False
return prices[-1] > sma and rsi < rsi_threshold
# 測試
closes = [95, 98, 102, 99, 96, 93, 91, 94, 97, 100,
103, 101, 98, 95, 92, 90, 88, 91, 95, 99, 103]
sma20 = calc_sma(closes, 20)
rsi14 = calc_rsi(closes, 14)
signal = is_buy_signal(closes)
print(f"SMA20:{sma20}")
print(f"RSI14:{rsi14}")
print(f"買入信號:{signal}")
煉金坊小叮嚀
自訂函數是從「寫程式」進化到「設計程式」的關鍵一步。我在量化開發中有一條鐵則:每當你發現同一段程式碼出現兩次,就立刻把它封裝成函數。這不只是節省行數,更重要的是未來只需修改一個地方,不會因為忘記同步更新而造成策略 bug。另外,函數設計要遵守「職責單一」原則——計算 RSI 的函數只負責計算,不要順便印出結果或寫入檔案;印出是呼叫者的責任。最後,養成為每個函數寫 docstring(三引號說明字串)的習慣,一個月後回來看自己的程式碼時,你會感謝當初的自己。