Python 裝飾器:修改函數的魔法 (第 13 章)

站主自己的課程,請大家支持
揭秘站長的架站心法:如何利用 Hugo × AI 打造高質感個人品牌網站? 揭秘站長的架站心法:如何利用 Hugo × AI 打造高質感個人品牌網站?
  • Post by
  • Dec 27, 2023
post-thumb

在閱讀開源專案 (如 Flask 或 Django) 時,你一定看過這種寫法:

@app.route("/")
def index():
    return "Hello"

那個 @ 符號到底是什麼?這就是 裝飾器 (Decorator)。它允許我們在不修改原函數程式碼的情況下,動態地增加功能。這聽起來像魔法,但其實只是「函數也是物件」特性的應用。

1. 預備知識:函數是第一類物件 (First-Class Object)

在 Python 中,函數 (Function) 就跟整數、字串一樣,可以:

  1. 被賦值給變數。
  2. 作為參數傳給另一個函數。
  3. 作為另一個函數的回傳值。
def shout(text):
    return text.upper()

def whisper(text):
    return text.lower()

# 高階函數:接收函數作為參數
def greet(func):
    greeting = func("Hi, Python")
    print(greeting)

greet(shout)    # HI, PYTHON
greet(whisper)  # hi, python

2. 閉包 (Closures)

在理解裝飾器前,必須先懂閉包。閉包是指一個內部函數 (Inner Function) 記住了外部函數 (Outer Function) 的變數環境。

def outer(msg):
    # 這是外部變數
    message = msg

    def inner():
        # 內部函數可以存取外部變數
        print(f"Message is: {message}")

    return inner  # 回傳內部函數本身 (沒加括號喔)

my_func = outer("Hello")
my_func()  # 輸出: Message is: Hello

即使 outer 執行完畢了,my_func (也就是 inner) 依然「記得」當時的 message 變數。這就是閉包。

3. 手工打造裝飾器

裝飾器本質上就是一個 接收函數並回傳新函數 的閉包。

假設我們想在執行某個函數前後,自動印出 Log:

def my_logger(func):
    def wrapper():
        print("--- 開始執行 ---")
        func()
        print("--- 執行結束 ---")
    return wrapper

def say_hello():
    print("Hello World!")

# 手動裝飾
decorated_hello = my_logger(say_hello)
decorated_hello()

輸出:

--- 開始執行 ---
Hello World!
--- 執行結束 ---

4. 語法糖:@ 符號

上面的 decorated_hello = my_logger(say_hello) 寫起來太囉唆。Python 提供了 @ 語法糖來簡化這一切。

@my_logger
def say_bye():
    print("Bye Bye!")

say_bye()

這段程式碼等同於手動裝飾,但更直觀。

5. 實戰:處理參數與回傳值

上面的 wrapper 沒有接收參數,如果原函數有參數怎麼辦?我們通常使用 *args**kwargs 來轉發所有參數。

範例:函數計時器

這是一個非常實用的裝飾器,用來測量函數執行時間。

import time
import functools

def timer(func):
    # 使用 @functools.wraps 保留原函數的 metadata (如 __name__, __doc__)
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)  # 執行原函數
        end_time = time.time()
        print(f"函數 {func.__name__} 執行花費: {end_time - start_time:.4f} 秒")
        return result  # 記得回傳原函數的結果!
    return wrapper

@timer
def heavy_computation(n):
    """模擬耗時運算"""
    total = 0
    for i in range(n):
        total += i
    return total

print(heavy_computation(1000000))

6. 總結

裝飾器是 Python 中實現 AOP (Aspect-Oriented Programming) 的主要手段。如果你發現有好幾個函數都有重複的「樣板程式碼」 (如權限檢查、Log 記錄、錯誤處理),試著把它們抽離出來寫成裝飾器吧!

本章重點回顧:

  • First-class function: 函數可以被傳遞。
  • Closure: 內部函數記住外部變數。
  • Syntactic Sugar: @decorator 只是 func = decorator(func) 的縮寫。
  • functools.wraps: 寫裝飾器時務必加上,以免原函數的名稱和文檔遺失。

下一章,我們將繼續深入函數式編程,介紹 Lambda, Map, Filter 與 Reduce,讓數據處理流水線化!


延伸閱讀

LATEST POST
TAG