裝飾

裝飾器 在 Python

{other: "%{count} 個練習"}

關於 裝飾器

裝飾器是一種函式,它接收另一個函式作為引數,目的是擴充或取代傳入函式的行為。 如果函式A是裝飾器,而函式B是它的引數,那麼函式A會修改、擴充或取代函式B的行為,而不修改_函式B的程式碼。 我們說裝飾器函式A包裝_了函式B。 雖然我們談的是「修改」行為,但被包裝的函式其實_並沒有被改變。 行為的改變方式有兩種:一種是把行為加在被包裝函式(以及它回傳的內容)的_周圍,另一種則是用其他行為_取代_被包裝函式的行為。

裝飾器是高階函式

高階函式是一種函式,它可以接受一個或多個函式作為引數,也可以回傳一個或多個函式。 一個函式如果被當作引數使用,或是由另一個函式回傳,它就是一級函式。 Python 函式是一種可呼叫物件,也是一級函式,可以像其他任何值或物件一樣存放在變數裡,或當作引數使用。 高階函式和一級函式一起合作,才讓裝飾器得以實現。

使用裝飾器是什麼樣子

把@符號加在裝飾器函式的名稱前面,並放在要被裝飾的函式正上方,像這樣:

@decorator
def decorated_function():
    pass

有些裝飾器會接受引數:

@decorator_with_arg(name="Bob")
def decorated_function2():
    pass

如果裝飾器定義了預設引數,就必須在@decorator()呼叫時加上括號,裝飾器才會運作,就像呼叫任何函式一樣:

@decorator_with_default_arg()
def decorated_function3():
    pass

如果裝飾器接受的是_位置_引數,沒有提供引數就會產生錯誤,看起來會像這樣:

TypeError: decorator_with_pos_arg() missing 1 required positional argument: 'name'

@decorator語法是語法糖,也就是一種簡寫,用來呼叫_裝飾函式_,並把_被裝飾的函式_當作引數傳給它。 以下是用其他方式呼叫裝飾器的範例:

def function():
    pass

function = decorator(function)


def function2():
    pass

function2 = decorator_with_arg(name="Bob")(function2)


def function3():
    pass

function3 = decorator_with_default_arg()(function3)

撰寫簡單的裝飾器

大多數裝飾器的用途是_擴充_或_取代_另一個函式的行為,不過有些裝飾器可能什麼都不做,只是把它包裝的函式回傳出去。

裝飾器是至少接受一個引數的函式,那個引數就是它所包裝的函式。 它們通常會回傳被包裝的函式,或是使用該函式的運算式的結果。

一個簡單的裝飾器(只是單純回傳它所包裝的函式)可以這樣寫:

>>> def do_nothing(func):
...     return func
...
... @do_nothing
... def function4():
...     return 4
...
>>> print(function4())
4

裝飾器也可以只加入副作用,例如用於記錄的額外資訊:

>>> def my_logger(func):
...     print(f"Entering {func.__name__}")
...     return func
...
... @my_logger
... def my_func():
...     print("Hello")
...
>>> my_func()
Entering my_func
Hello

裝飾器不會回傳它自己。 它可以回傳它的函式引數、另一個函式,或是一個或多個值,用來取代傳入或被裝飾函式的回傳內容。 如果裝飾器回傳另一個函式,通常會回傳一個內部函式。

內部函式

函式可以定義在另一個函式裡面。 這種巢狀的函式稱為內部函式。 裝飾器可以用內部函式來包裝它的函式引數。 接著裝飾器回傳這個內部函式。 內部函式再回傳原本的函式引數。

使用內部函式的驗證裝飾器

以下是裝飾器用於驗證的範例:

def my_validator(func):
    def my_wrapper(world):
        print(f"Entering {func.__name__} with {world} argument")
        
        if world == "Pluto":
            print("Pluto is not a planet!")
        else:
            return func(world)
    return my_wrapper

@my_validator
def my_func(planet):
    print(f"Hello, {planet}!")

my_func("World")
#-> Entering my_func with World argument
#-> Hello, World!

my_func("Pluto")
#-> Entering my_func with Pluto argument
#-> Pluto is not a planet!

第一行是裝飾器func引數的定義。 下一行則是裝飾器_內部函式_的定義,它負責包裝func引數。 由於_內部函式_包裝了裝飾器的func引數,所以傳給func的引數也會傳給它。 注意,包裝函式不一定要使用和func裡定義相同的引數名稱。 原始函式用的是planet,裝飾器用的是world,而裝飾器依然可以正常運作。

內部函式會回傳func;而如果world == "Pluto",則會印出冥王星不是行星。 也可以改寫成拋出ValueError。 也就是說,內部函式_包裝了func,並且回傳func,或是做出某個取代func原本行為的動作。 裝飾器會回傳它的_內部函式。 _內部函式_可能會回傳原本傳入的函式,也可能不會。 端看包裝函式或_內部函式_中哪些程式碼會依條件執行,可能回傳func、可能拋出錯誤,也可能回傳func回傳型別的值。

裝飾接受任意數量引數的函式

使用*args和**kwargs語法,就能為接受任意數量引數的函式撰寫裝飾器。

以下是針對接受任意數量引數的函式所寫的裝飾器範例:

def double(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs) * 2
    return wrapper

@double
def add(*args):
    return sum(args)

print(add(2, 3, 4))
#-> 18

print(add(2, 3, 4, 5, 6))
#-> 40

這樣就能把函式引數的回傳值加倍。 如果想讓回傳值變三倍、四倍等等,可以為裝飾器本身加上一個參數,下一節會示範。

帶有自己參數的裝飾器

以下是一個可以設定成將被裝飾函式的回傳值乘以任意倍數的裝飾器範例:

def multi(factor=1):
     if factor == 0:
        raise ValueError("factor must not be 0")

     def outer_wrapper(func):
         def inner_wrapper(*args, **kwargs):
             return func(*args, **kwargs) * factor
         return inner_wrapper
     return outer_wrapper

@multi(factor=3)
def add(*args):
    return sum(args)

print(add(2, 3, 4))
#-> 27

print(add(2, 3, 4, 5, 6))
#-> 60

最前面的幾行會驗證factor不是0。 接著定義外層包裝函式。 它的簽章和我們對無參數裝飾器的預期相同。 外層包裝函式有一個內部函式,其簽章和原始函式相同。 內層包裝函式負責把原始函式的回傳值乘上傳給裝飾器的引數。 外層包裝函式回傳內層包裝函式,而裝飾器回傳外層包裝函式。

以下是一個帶參數的裝飾器範例,它可以控制是否驗證傳給原始函式的引數:

def check_for_pluto(check=True):
    def my_validator(func):
        def my_wrapper(world):
            print(f"Entering {func.__name__} with {world} argument")
            if check and world == "Pluto":
                print("Pluto is not a planet!")
            else:
                return func(world)
        
        return my_wrapper
    return my_validator

@check_for_pluto(check=False)
def my_func(planet):
    print(f"Hello, {planet}!")

my_func("World")
#-> Entering my_func with World argument
#-> Hello, World!

my_func("Pluto")
#-> Entering my_func with Pluto argument
#-> Hello, Pluto!

這樣就能輕鬆切換要不要檢查Pluto,而且不需要修改my_func。

透過 GitHub 編輯 連結會在新視窗或分頁中開啟