裝飾器入門:Python裝飾器如何給函數“加功能”?

在Python中,函數是“一等公民”——它可以被當作參數傳遞、賦值給變量,甚至作爲返回值。裝飾器正是利用了這一特性,讓我們能在不修改原函數代碼的前提下,給函數“動態添加功能”。這就像給禮物盒套上一個漂亮的包裝紙,既保留了禮物本身,又增添了新的外觀。

一、爲什麼需要裝飾器?

假設我們要給多個函數添加“打印執行日誌”的功能。如果直接修改每個函數,代碼會變得重複且難以維護。例如:

# 原始函數1
def add(a, b):
    return a + b

# 原始函數2
def subtract(a, b):
    return a - b

如果想給它們加日誌,最直接的方式是手動修改每個函數:

# 加了日誌的add函數
def add(a, b):
    print(f"函數add開始執行,參數:a={a}, b={b}")
    result = a + b
    print(f"函數add執行結束,結果:{result}")
    return result

# 加了日誌的subtract函數(重複代碼)
def subtract(a, b):
    print(f"函數subtract開始執行,參數:a={a}, b={b}")
    result = a - b
    print(f"函數subtract執行結束,結果:{result}")
    return result

這樣做顯然不優雅。如果有100個函數,就要重複寫100遍日誌代碼。裝飾器的出現,就是爲了解決這種“重複造輪子”的問題

二、裝飾器的基本原理

裝飾器本質是一個函數,它接收一個函數作爲參數,並返回一個新函數(這個新函數“包裝”了原函數,添加了額外功能)。

1. 最簡單的裝飾器

我們先定義一個“日誌裝飾器”,它能給任何函數添加“開始/結束執行”的日誌:

def log_decorator(func):  # 接收原函數func
    def wrapper(*args, **kwargs):  # 內部函數(包裝器)
        # 1. 添加功能:打印開始日誌
        print(f"函數 {func.__name__} 開始執行,參數:{args}, {kwargs}")
        # 2. 調用原函數
        result = func(*args, **kwargs)
        # 3. 添加功能:打印結束日誌
        print(f"函數 {func.__name__} 執行結束,結果:{result}")
        return result
    return wrapper  # 返回包裝後的函數

2. 使用裝飾器

@ 語法糖(裝飾器語法)把原函數“包裹”起來:

@log_decorator  # 相當於:add = log_decorator(add)
def add(a, b):
    return a + b

@log_decorator  # 同理:subtract = log_decorator(subtract)
def subtract(a, b):
    return a - b

3. 調用測試

現在調用 add(1, 2) 時,實際執行的是 wrapper 函數,會自動打印日誌:

add(1, 2)
# 輸出:
# 函數 add 開始執行,參數:(1, 2), {}
# 函數 add 執行結束,結果:3

三、裝飾器的核心細節

1. *args**kwargs

*args 接收位置參數(如 add(1, 2) 中的 1, 2),**kwargs 接收關鍵字參數(如 add(a=1, b=2) 中的 a=1, b=2)。這確保裝飾器能適配任何參數的函數。

2. 保留原函數信息

如果直接用上述裝飾器,add.__name__ 會變成 wrapper(因爲 add 被替換成了 wrapper)。這時可以用 functools.wraps 保留原函數的元信息:

from functools import wraps

def log_decorator(func):
    @wraps(func)  # 保留原函數信息
    def wrapper(*args, **kwargs):
        print(f"函數 {func.__name__} 開始執行")
        result = func(*args, **kwargs)
        print(f"函數 {func.__name__} 執行結束")
        return result
    return wrapper

# 現在查看函數名
print(add.__name__)  # 輸出:add(而非 wrapper)

四、帶參數的裝飾器

如果需要給裝飾器傳遞參數(如不同的日誌前綴),可以嵌套一層函數:

def log_decorator(prefix="【日誌】"):  # 外層函數:接收裝飾器參數
    def decorator(func):  # 內層函數:接收原函數
        @wraps(func)
        def wrapper(*args, **kwargs):
            print(f"{prefix} 函數 {func.__name__} 開始執行")
            result = func(*args, **kwargs)
            print(f"{prefix} 函數 {func.__name__} 執行結束")
            return result
        return wrapper
    return decorator  # 返回內層裝飾器

# 使用帶參數的裝飾器
@log_decorator(prefix="【調試】")
def multiply(a, b):
    return a * b

multiply(3, 4)
# 輸出:
# 【調試】 函數 multiply 開始執行
# 【調試】 函數 multiply 執行結束

五、裝飾器的應用場景

裝飾器非常靈活,常見場景包括:
- 日誌記錄:記錄函數調用時間、參數、返回值。
- 性能測試:統計函數執行耗時(如 @timer_decorator)。
- 權限驗證:調用函數前檢查用戶權限(如 @check_permission)。
- 緩存:緩存函數結果,避免重複計算(如 @cache_decorator)。

六、多個裝飾器的執行順序

如果多個裝飾器作用於同一函數,執行順序是從下往上(靠近函數的裝飾器先執行)。例如:

@decorator2
@decorator1
def func():
    pass

執行順序是:decorator2decorator1funcdecorator1decorator2

總結

裝飾器是Python的“魔法工具”,通過函數嵌套和閉包思想,讓我們能優雅地給函數添加功能。核心要點:
1. 裝飾器是接收函數的函數,返回新函數。
2. @ 語法糖簡化了裝飾器的使用。
3. *args**kwargs 處理任意參數,functools.wraps 保留原函數信息。
4. 帶參數的裝飾器需嵌套兩層函數。

從簡單的日誌到複雜的權限驗證,裝飾器讓代碼更簡潔、可維護。現在,你可以嘗試給一個自己的函數加上日誌裝飾器,體驗它的便捷了!

小夜