在 Python 的世界裏,裝飾器是一個聽起來可能有些高深,但實際上卻異常強大和優雅的工具。它允許你在不修改原函數代碼的情況下,為其增加額外的功能。無論是日誌記錄、性能測試,還是權限校驗,裝飾器都能以一種簡潔、可複用的方式實現。

本文將帶你從零開始,一步步深入理解 Python 裝飾器的原理與應用。

1. 裝飾器是什麼?為什麼要用它?

想象一下,你有一個函數 say_hello()

def say_hello():
    print("Hello, Python!")

現在,你的老闆要求你在調用這個函數前後,都打印一行日誌來記錄執行時間。最直接的做法是修改函數內部:

import time

def say_hello():
    start_time = time.time()
    print("Hello, Python!")
    end_time = time.time()
    print(f"Function executed in {end_time - start_time:.4f} seconds.")

say_hello()

這樣做雖然可行,但有幾個明顯的缺點:

  1. 違反開閉原則:對修改開放,對擴展關閉。我們不應該為了增加新功能而去修改已經穩定運行的代碼。
  2. 代碼冗餘:如果有很多函數都需要這個日誌功能,你就得在每個函數裏都複製粘貼這段代碼,維護起來將是災難。
  3. 業務邏輯混雜say_hello() 的核心職責是打印問候語,但現在它還承擔了計時的職責,邏輯變得混亂。

裝飾器就是為了解決這類問題而生的。 它是一種設計模式,允許你將“橫切關注點”(Cross-cutting Concerns,如日誌、緩存、權限等)與核心業務邏輯分離開來。

2. 理解裝飾器的基石:函數是“一等公民”

要理解裝飾器,必須先理解 Python 的一個核心理念:函數是“一等公民”(First-Class Citizen)。這意味着函數可以像普通變量一樣被:

  • 賦值給變量
  • 作為參數傳遞給其他函數
  • 作為其他函數的返回值
  • 存儲在數據結構中(如列表、字典)

讓我們看一個簡單的例子:

def greet(name):
    return f"Hello, {name}!"

# 1. 賦值給變量
say_greet = greet
print(say_greet("Alice"))  # 輸出: Hello, Alice!

# 2. 作為參數傳遞
def call_func(func, arg):
    print("I am going to call a function now.")
    print(func(arg))
    print("Function call finished.")

call_func(greet, "Bob") 
# 輸出:
# I am going to call a function now.
# Hello, Bob!
# Function call finished.

# 3. 作為返回值
def get_greeter():
    return greet

my_greeter = get_greeter()
print(my_greeter("Charlie")) # 輸出: Hello, Charlie!

正是因為函數如此靈活,我們才有可能用一個函數去“包裝”另一個函數。

3. 手寫一個簡單的裝飾器

現在,讓我們用函數作為一等公民的特性,來手動實現一個裝飾器的功能。

我們的目標是:創建一個新函數,它能包裝 say_hello(),並在其前後添加計時邏輯。

import time

def say_hello():
    """原始函數"""
    time.sleep(1) # 模擬耗時操作
    print("Hello, Python!")

# 這是我們的“裝飾器”函數
def timer_decorator(func):
    """一個計時裝飾器"""
    def wrapper():
        start_time = time.time()
        func()  # 調用原始函數
        end_time = time.time()
        print(f"Function '{func.__name__}' executed in {end_time - start_time:.4f} seconds.")
    return wrapper # 返回包裝後的新函數

# 使用我們的裝飾器
# 1. 獲取原始函數
original_say_hello = say_hello

# 2. 將原始函數傳遞給裝飾器,得到一個新函數
decorated_say_hello = timer_decorator(original_say_hello)

# 3. 調用這個新函數
decorated_say_hello()

運行結果:

Hello, Python!
Function 'say_hello' executed in 1.0012 seconds.

看!我們沒有修改 say_hello() 的任何代碼,就成功地給它增加了計時功能。timer_decorator 就是一個裝飾器,它接收一個函數作為參數,返回一個新的函數 wrapper,這個 wrapper 函數包含了原始函數的調用以及我們想要添加的新功能。

4. Python 的語法糖:@ 符號

手動傳遞函數和接收新函數雖然可行,但 Python 提供了更優雅的語法糖——@ 符號。

上面的代碼可以改寫為:

import time

def timer_decorator(func):
    def wrapper():
        start_time = time.time()
        func()
        end_time = time.time()
        print(f"Function '{func.__name__}' executed in {end_time - start_time:.4f} seconds.")
    return wrapper

@timer_decorator  # 這就是語法糖!
def say_hello():
    time.sleep(1)
    print("Hello, Python!")

# 直接調用即可,Python 會自動完成 say_hello = timer_decorator(say_hello) 的過程
say_hello()

@timer_decorator 這行代碼的作用等同於 say_hello = timer_decorator(say_hello)。它告訴 Python:“請用 timer_decorator 來裝飾下面這個函數,並將裝飾後的結果重新賦值給原來的函數名。” 這樣,調用 say_hello() 時,實際上是在調用被 wrapper 包裝後的版本。

5. 進階:處理帶參數的函數

我們上面的裝飾器只能用於沒有參數的函數。如果原函數有參數,比如 greet(name),我們的 wrapper 就會報錯。

為了讓裝飾器更具通用性,我們需要使用 *args**kwargs 來接收任意數量的位置參數和關鍵字參數。

def timer_decorator(func):
    def wrapper(*args, **kwargs):  # 接收任意參數
        start_time = time.time()
        result = func(*args, **kwargs)  # 將參數傳遞給原始函數
        end_time = time.time()
        print(f"Function '{func.__name__}' executed in {end_time - start_time:.4f} seconds.")
        return result # 返回原始函數的執行結果
    return wrapper

@timer_decorator
def greet(name, greeting="Hello"):
    time.sleep(0.5)
    return f"{greeting}, {name}!"

message = greet("David", greeting="Good morning")
print(message)

運行結果:

Function 'greet' executed in 0.5012 seconds.
Good morning, David!

現在,timer_decorator 可以裝飾任何函數了,無論它有多少參數。注意,我們還返回了 func() 的結果,這保證了裝飾器不會改變原函數的返回值。

6. 終極挑戰:帶參數的裝飾器

有時候,我們希望裝飾器本身也能接收參數。例如,我們想創建一個日誌裝飾器,可以自定義日誌的級別。

def logit(level='INFO'):
    """一個帶參數的裝飾器工廠"""
    def decorator(func):
        def wrapper(*args, **kwargs):
            print(f"[{level}] - About to run {func.__name__}...")
            result = func(*args, **kwargs)
            print(f"[{level}] - {func.__name__} finished.")
            return result
        return wrapper
    return decorator

@logit(level='DEBUG')
def add(a, b):
    return a + b

print(add(2, 3))

運行結果:

[DEBUG] - About to run add...
[DEBUG] - add finished.
5

這裏的邏輯多了一層:

  1. logit(level='DEBUG') 被調用,它返回一個真正的裝飾器 decorator
  2. Python 接着用這個返回的 decorator 去裝飾 add 函數,相當於 add = decorator(add)
  3. 最終,add 指向了 wrapper 函數。

所以,帶參數的裝飾器實際上是一個返回裝飾器的函數,我們稱之為“裝飾器工廠”。

7. 一個重要的細節:functools.wraps

你可能會發現,被裝飾後的函數,它的元信息(如函數名 __name__、文檔字符串 __doc__)丟失了。

@timer_decorator
def say_hello():
    """A simple greeting function."""
    print("Hello!")

print(say_hello.__name__)  # 輸出: wrapper
print(say_hello.__doc__)   # 輸出: None

這是因為我們的 wrapper 函數覆蓋了原函數的元信息。為了解決這個問題,Python 提供了 functools.wraps。它本身也是一個裝飾器,用於將原函數的元信息複製到 wrapper 函數上。

最佳實踐:總是在你自己的 wrapper 函數上使用 @functools.wraps(func)

import functools
import time

def timer_decorator(func):
    @functools.wraps(func)  # 使用 functools.wraps
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"Function '{func.__name__}' executed in {end_time - start_time:.4f} seconds.")
        return result
    return wrapper

@timer_decorator
def say_hello():
    """A simple greeting function."""
    print("Hello!")

print(say_hello.__name__)  # 輸出: say_hello
print(say_hello.__doc__)   # 輸出: A simple greeting function.

現在,元信息被完美地保留了!

總結

Python 裝飾器是一種基於函數閉包和“函數是第一公民”特性的強大工具。它通過包裝函數,讓我們能夠以非侵入式的方式為代碼添加橫切關注點,極大地提升了代碼的模塊化、可讀性和可複用性。

核心要點回顧:

  1. 本質:裝飾器是一個接收函數、返回新函數的函數。
  2. 語法糖@decoratorfunc = decorator(func) 的簡寫。
  3. 通用性:使用 *args**kwargs 讓裝飾器能處理任意參數的函數。
  4. 參數化:帶參數的裝飾器是一個“裝飾器工廠”,需要多一層嵌套函數。
  5. 最佳實踐:始終使用 @functools.wraps(func) 來保留原函數的元信息。

掌握了裝飾器,你就掌握了 Python 編程中一種非常高級和地道的技巧。它不僅能讓你的代碼更優雅,更能體現你對 Python 核心思想的深刻理解。現在就去試試用它來優化你的項目吧!