深入理解 Python 裡的多層裝飾器設計與應用

更新日期: 2025 年 1 月 23 日

本文為 python 裝飾器系列文第四篇:

  1. 深入理解 Python 中的一等公民
  2. 理解 Python 裡的 @deprecated 裝飾器及其應用
  3. 深入理解 Python 裝飾器中的參數傳遞與運作原理 
  4. 深入理解 Python 裡的多層裝飾器設計與應用 👈進度

裝飾器是 Python 中用於增強函數功能的重要工具。

通常,裝飾器只接收函數作為參數,但如果希望裝飾器還能接收額外的參數來進一步自訂功能,該怎麼實現呢?

本文將介紹如何實現支持額外參數的裝飾器設計,並探討如何進一步優化多層裝飾器的使用方式。


設計支持參數的裝飾器

當我們希望裝飾器能接收額外參數(如提示函數過期的原因)時,可以通過外層函數包裹內層裝飾器來實現。

以下是一個簡單的範例:

基本多層裝飾器

def deprecated(reason=None):  # 外層函數接收額外參數
    def deprecator(fn):  # 內層裝飾器接收被裝飾的函數
        def wrapper(*args, **kwargs):  # 包裝函數
            print(f"函數 {fn.__name__} 要過期了! {reason}")
            return fn(*args, **kwargs)  # 執行原函數
        return wrapper
    return deprecator

使用範例

@deprecated(reason="不再需要此功能")
def hi(a, b):
    print(a, b)
    print("hello")

hi(1, 2)

執行結果:

函數 hi 要過期了! 不再需要此功能
1 2
hello

深入理解運作原理

核心觀念

  1. Python 裡的函數是 “一等公民”
    意思是函數可以像變數一樣被傳遞、返回,或者作為其他函數的參數。
  2. 裝飾器的基本運作
    裝飾器的本質是:接收一個函數作為參數,返回一個包裝過的函數。
  3. 分層裝飾器的運作
    多層裝飾器(如本例)允許裝飾器本身接收額外的參數,這是一種進階用法。

為何運作

執行流程:

1. @deprecated(reason="不再需要此功能") 裝飾:
hi = deprecated("不再需要此功能")(hi)
    |
    +---> deprecated() ---> 返回 deprecator 函數
                           |
                           +---> deprecator(hi) ---> 返回 wrapper 函數

2. 呼叫 hi(1,2):
hi(1,2) ---> wrapper(1,2)
            |
            +---> 印出 "函數 hi 要過期了! 不再需要此功能"  
            |
            +---> fn(1,2) [原始 hi 函數]
                  |
                  +---> 印出 "1 2"
                  +---> 印出 "hello"

讓我們逐步解析這段程式碼的運作順序,看看電腦如何執行:

1. @deprecated(reason="不再需要此功能")

當這段程式碼執行時:

  • Python 將 @deprecated(reason="不再需要此功能") 替換為:
  hi = deprecated(reason="不再需要此功能")(hi)

2. deprecated(reason="不再需要此功能")

這行程式碼執行時:

  • reason="不再需要此功能" 被傳遞到 deprecated 函數中。
  • deprecated 函數執行,返回內層函數 deprecator
    此時:
  deprecator = deprecated(reason="不再需要此功能")

3. deprecator(hi)

執行到這一步時:

  • hi(原始函數)被傳遞到 deprecator(fn),成為參數 fn
  • deprecator(fn) 執行,返回包裝函數 wrapper

4. 替換 hi

hi 函數被替換為 wrapper 函數。
此時,程式相當於:

def wrapper(*args, **kwargs):
    print(f" 函數 {hi.__name__} 要過期了! 不再需要此功能")
    return hi(*args, **kwargs)

解釋 deprecatorreturn 核心概念

關鍵在於:deprecator(fn) 不會執行 fn(原函數),而是返回包裝好的 wrapper 函數

  • deprecated(reason) 返回 deprecator 時,這時候的 deprecator 還沒有執行。
  • deprecator(fn) 執行時,它也只返回 wrapper,而不是立即執行 wrapper

電腦執行順序確實是自上而下的,但「返回」的是物件的引用,而不是函數的執行結果。

return 是用來把函數的執行結果 “交還” 給呼叫者

  • 它可以返回:
  1. 一個值(數字、字串、布林值等)。
  2. 一個物件(包括列表、字典、函數本身等)。
  3. None(如果函數沒有顯式使用 return,默認返回 None)。

關鍵是:return 不會執行返回的東西,而是把返回的內容傳遞給呼叫它的程式。

return 不執行,只返回引用

函數裡的 return 語句本質上是將某個東西(值或物件)傳遞給程式的下一步,並結束函數的執行。

這並不會執行被返回的東西,除非呼叫者進一步執行它。

範例:返回數字

def add(a, b):
    return a + b

result = add(2, 3)  # `add` 返回 2 + 3 的結果,結果是 5
print(result)  # 打印 5

執行順序:

  1. 執行 add(2, 3)
  2. return a + b 把結果 5 傳回給 result
  3. 打印 result

範例:返回函數

def outer():
    def inner():
        return "Hello from inner"
    return inner

fn = outer()  # outer 返回內部函數 inner,而不是執行 inner
print(fn)     # 打印 <function outer.<locals>.inner at 0x...>
print(fn())   # 呼叫 fn(其實是 inner 函數),執行並打印返回值 "Hello from inner"

執行順序:

  1. outer() 執行並返回 inner 函數的引用,fn 指向 inner
  2. print(fn) 顯示 inner 是一個函數物件。
  3. print(fn()) 執行 inner 函數,打印 "Hello from inner"

範例:多層函數與裝飾器

def outer(reason):
    def deprecator(fn):
        def wrapper(*args, **kwargs):
            print(f"警告:{fn.__name__} 即將過期!原因:{reason}")
            return fn(*args, **kwargs)
        return wrapper
    return deprecator

@outer("此功能將移除")
def say_hello(name):
    print(f"Hello, {name}!")

say_hello("Alice")

執行順序:

  1. @outer("此功能將移除") 等價於 say_hello = outer("此功能將移除")(say_hello)
  2. outer("此功能將移除") 返回 deprecator,且 reason 被記住。
  3. deprecator(say_hello) 被執行,返回包裝函數 wrapper
  4. say_hello("Alice") 實際執行 wrapper 函數。

為什麼 reason 會被記住?

這是因為 Python 的閉包(Closure)機制。當 outer 函數執行時,它創建了一個作用域,其中包含 reason 變數。

內部函數 deprecatorwrapper 可以訪問這個外部作用域的變數。

簡單來說:

outer("此功能將移除")
    |
    +--- reason 存在記憶體中
         |
         +--- deprecator 可以訪問 reason
              |
              +--- wrapper 也可以訪問 reason

這就像把 reason 封存在一個記憶體空間中,任何在這個空間內定義的函數都能訪問它,即使 outer 函數已經執行完畢。這種特性稱為閉包。

多層函數與 return 的本質

在多層函數(像裝飾器)中,return 的本質是分階段返回不同的函數引用

範例解釋 return 一層一層返回

def A(param1):
    print("進入 A")
    def B(param2):
        print("進入 B")
        def C(param3):
            print("進入 C")
            return f"A={param1}, B={param2}, C={param3}"
        return C
    return B

# 呼叫 A,返回 B
func1 = A("我是A")
# 呼叫 B,返回 C
func2 = func1("我是B")
# 呼叫 C,返回結果
result = func2("我是C")
print(result)

執行過程:

  1. 執行 A("我是A")
    • 打印 進入 A
    • 返回函數 B,變數 func1 持有 B 的引用。
  1. 執行 func1("我是B")(實際執行 B("我是B")):
    • 打印 進入 B
    • 返回函數 C,變數 func2 持有 C 的引用。
  1. 執行 func2("我是C")(實際執行 C("我是C")):
    • 打印 進入 C
    • 返回結果 "A=我是A, B=我是B, C=我是C"
  1. 最終打印 "A=我是A, B=我是B, C=我是C"

代碼的裝飾器實際上分三步執行:

  1. deprecated(reason):返回 deprecator 函數。
  2. deprecator(fn):返回包裝函數 wrapper
  3. wrapper 被執行時,才真正執行你的原始函數。

這一切的關鍵在於:

  • return 是在返回 “物件的引用”,不是執行物件本身
  • 每一層的返回都只是傳遞一個函數引用,直到最終執行最內層的函數。

提升使用便利性

上述設計要求在使用裝飾器時,必須顯式加上括號,即使不需要額外參數,也要寫成 @deprecated()

若省略括號(即寫成 @deprecated),則會發生錯誤。

這是因為 @deprecated 將直接執行內層裝飾器,而非返回裝飾器本身。

解決方法:條件判斷

為了讓裝飾器既支持接收參數,又能在省略括號時正常工作,可以在外層函數中添加判斷:

def deprecated(reason=None):
    if callable(reason):  # 判斷是否直接傳入函數
        fn = reason
        reason = None
        def wrapper(*args, **kwargs):
            print(f" 函數 {fn.__name__} 要過期了!")
            return fn(*args, **kwargs)
        return wrapper
    else:  # 否則按照多層裝飾器處理
        def deprecator(fn):
            def wrapper(*args, **kwargs):
                print(f" 函數 {fn.__name__} 要過期了! {reason}")
                return fn(*args, **kwargs)
            return wrapper
        return deprecator

使用範例

情境 1:指定原因

@deprecated(reason="此功能將於 2024 年移除")
def hi(a, b):
    print(a, b)
    print("hello")

hi(1, 2)

執行結果:

 函數 hi 要過期了! 此功能將於 2024 年移除
1 2
hello

情境 2:省略括號

@deprecated
def hi(a, b):
    print(a, b)
    print("hello")

hi(1, 2)

執行結果:

 函數 hi 要過期了!
1 2
hello

運作原理

  1. @deprecated 被調用時,reason 是函數本身,觸發 if callable(reason) 的條件,返回包裝函數。
  2. @deprecated(reason="...") 被調用時,reason 是一個字串,外層函數返回內層裝飾器。

這種設計兼顧了使用便利性和功能擴展性,適用於多數開發場景。


總結

多層裝飾器為我們提供了一種靈活的方式來設計功能更強大的裝飾器。

通過添加條件判斷,我們可以讓裝飾器既支持接收額外參數,又能在省略括號時正常使用。

這樣的設計不僅提升了使用體驗,還擴展了裝飾器的適用範圍。

熟練掌握這種裝飾器設計方法,將讓你的 Python 編程更加簡潔、高效且優雅!

Similar Posts