13 Python物件導向程式設計:裝飾器

救苦救难韩天尊發表於2024-09-05

本篇是 Python 系列教程第 13 篇,更多內容敬請訪問我的 Python 合集

Python 裝飾器是一種強大的工具,用於修改或增強函式或方法的行為,而無需更改其原始碼。裝飾器本質上是一個接收函式作為引數的函式,並返回一個新的函式。裝飾器的用途包括日誌記錄、效能測試、事務處理、快取、許可權校驗等

1 基本語法

裝飾器的基本語法是在函式定義之前使用@符號,緊跟著裝飾器的名字。例如:

# 定義一個裝飾器,引數為被裝飾的方法
def my_decorator(func):
    def wrapper():
        print("方法執行前")
        func()
        print("方法執行後")

    return wrapper

# 用“@”使用裝飾器
@my_decorator
def say_hello():
    print("Hello!")

say_hello()

這段程式碼會輸出:

方法執行前
Hello!
方法執行後

2 引數傳遞

如果被裝飾的函式需要引數,裝飾器也需要相應地處理這些引數:

def my_decorator(func):
    def wrapper(name):
        print("方法執行前")
        func(name)
        print("方法執行後")

    return wrapper

@my_decorator
def greet(name):
    print(f"Hello, {name}!")

greet("Alice")

輸出:

方法執行前
Hello, Alice!
方法執行後

引數可以用可變引數,比較靈活,如下:

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("方法執行前")
        func(*args, **kwargs)
        print("方法執行後")

    return wrapper

@my_decorator
def greet(name):
    print(f"Hello, {name}!")

greet("Alice")

3 使用多個裝飾器

你可以為同一個函式使用多個裝飾器:

def decorator_A(func):
    print("enter A")
    def wrapper():
        print("Before A")
        func()
        print("After A")
    print("out A")
    return wrapper
 
def decorator_B(func):
    print("enter B")
    def wrapper():
        print("Before B")
        func()
        print("After B")
    print("out B")
    return wrapper
 
@decorator_A
@decorator_B
def my_function():
    print("Inside my_function")
 
# 執行被裝飾器裝飾的函式
my_function()

輸出:

enter B
out B
enter A
out A
Before A
Before B
Inside my_function
After B
After A

注意列印結果的順序。

為了方便表達我們先把靠近被修飾方法的裝飾器叫內層裝飾器,如示例中的@decorator_B,不靠近的叫外層裝飾器,如示例中的@decorator_A

在閉包wrapper外面的程式碼是內層裝飾器先執行,在閉包wrapper內部的程式碼執行順序複雜一些:①外層裝飾器先執行func() 前面的程式碼->②內層裝飾器執行func() 前面的程式碼->③執行func() ->④內層裝飾器執行func() 後面的程式碼->⑤外層裝飾器執行func() 後面的程式碼。

4 給裝飾器傳參

裝飾器本身可以接受引數,可以根據傳入的不同引數來改變裝飾器的行為。

前面的例子都是沒有引數的裝飾器,如果我們想要給裝飾器傳參該怎麼辦呢?於是我們就思考一下,什麼東東可以接收引數呢,答案是函式。bingo!Python也是這樣設計的,我們只需要在裝飾器外面包裹一層函式,就可以把引數傳遞給函式進而傳遞給裝飾器了。

可以這樣定義裝飾器:

def repeat(num_times):
    def decorator_repeat(func):
        def wrapper(*args, **kwargs):
            for _ in range(num_times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator_repeat

@repeat(3)
def greet(name):
    print(f"Hello, {name}!")

greet("Alice")

輸出:

Hello, Alice!
Hello, Alice!
Hello, Alice!

這樣就定義了一個根據傳入裝飾器的數值執行指定次數函式的裝飾器。

5 類作為裝飾器

5.1 __call__方法

裝飾器不僅僅可以是方法,也可以是類。這就不得不介紹一個特殊的方法__call__

Python的類只要實現了__call__ 這個特殊方法,類的例項物件就可以像函式一樣被呼叫,因為當嘗試把物件寫成方法呼叫的寫法時(名稱+()),Python 直譯器會查詢該物件的 __call__ 方法並呼叫它。

下面來看一個簡單的例子,演示__call__的使用:

class Counter:
    def __init__(self):
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"方法被呼叫了 {self.count} 次")

counter = Counter()

# 模擬呼叫
counter()
counter()
counter()

列印:

方法被呼叫了 1 次
方法被呼叫了 2 次
方法被呼叫了 3 次

5.2 類作為裝飾器

類作為裝飾器的一個主要優勢是可以方便地維護狀態,因為類可以有例項變數。

理解了__call__之後,我們可以想到類作為裝飾器的原理是在類裡實現了__call__方法,使得裝飾器的程式碼可以被執行。

下面我們定義一個記錄函式呼叫次數的裝飾器:

class CallCounter:
    def __init__(self, func):
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"{self.func.__name__} 被呼叫了 {self.count} 次")
        return self.func(*args, **kwargs)

@CallCounter
def say_hello(name):
    print(f"Hello, {name}!")

# 呼叫被裝飾的函式
say_hello("Alice")
say_hello("Bob")
say_hello("Charlie")

# 輸出
# say_hello 被呼叫了 1 次
# Hello, Alice!
# say_hello 被呼叫了 2 次
# Hello, Bob!
# say_hello 被呼叫了 3 次
# Hello, Charlie!

程式碼解釋:

  1. CallCounter 類有一個建構函式 __init__,它必須接受一個函式作為引數。
  2. 類實現了 __call__ 方法,這使得其例項可以像函式一樣被呼叫。
  3. __call__ 方法中,每次呼叫被裝飾的函式時,都會增加計數器 count 的值,並列印出函式被呼叫的次數。
  4. 最後,__call__ 方法呼叫了原始函式 self.func 並返回結果。

相關文章