Python 自訂函數:def、參數與 return 回傳值設計

Python 自訂函數(def)讓你把一段可重複使用的程式碼封裝成有名字的單元,呼叫時只需一行,大幅減少重複程式碼、提升可讀性與維護性。本文完整解析 def 的定義語法、位置參數與預設值參數的設計、return 回傳值的運作機制、區域變數與全域變數的作用域規則,以及如何設計出乾淨、職責單一的函數,附量化策略指標計算函數實戰範例。

Cinematic dark alchemy forge with a glowing sealed arcane capsule engraved with mystical runes representing an encapsulated Python function, amber and gold firelight illuminating input ingredients entering one side and refined output emerging from the other, deep dramatic shadows on ancient stone walls, cinematic lighting, 16:9

什麼是自訂函數?

自訂函數是用 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(三引號說明字串)的習慣,一個月後回來看自己的程式碼時,你會感謝當初的自己。

張貼留言

較新的 較舊