Python裝飾器模式

banq發表於2022-05-13

在Python中,函式是一等公民,裝飾器是強大的語法糖,利用這一功能給程式設計師提供了一種看似 "神奇 "的方式來建構函式和類的有用組合。
這是一個重要的語言特性,它使 Python 與傳統的 OOP 語言如 C++ 和 Java 區別開來,後者實現這種功能需要更多的程式碼,或者更復雜的模板化程式碼。
與C++這樣的語言相比,Python的這種動態特性產生了更多的執行時開銷,但它使程式碼更容易編寫和理解。
這對程式設計師和專案來說是一個勝利;在大多數現實世界的軟體工程工作中,執行時效能不是一個瓶頸。

在 Python 中,函式是第一等公民:函式可以傳遞給其他函式,可以從函式中返回,並且可以即時建立。讓我們看一個例子。

# 即時定義一個函式
pow2 = lambda x: x**2
print(pow2(2))

# 將一個函式作為引數
def print_twice(func: Callable, arg: Any):
    print(func(arg))
    print(func(arg))
print_twice(pow2, 3)

# 將一個函式作為引數,並返回一個新函式
def hello():
    print('Hello world!')
def loop(func: Callable, n: int):
    for _ in range(n):
        func()
loop(hello, 3)

輸出:
4
9
9
Hello world!
Hello world!
Hello world!

Pythons中的裝飾器是語法糖,用於將函式傳遞給函式並返回一個新函式。

這篇文章的程式碼在Github上。

@measure:沒有引數的裝飾器函式
讓我們舉一個有用的例子,測量執行一個函式需要多長時間。最好的情況是,我們可以很容易地註解一個現有的函式,並獲得 "免費 "的測量。讓我們來看看下面兩個函式。

from timeit import default_timer as timer
from time import sleep

def measure(func: Callable):
    def inner(*args, **kwargs):
        print(f'---> Calling {func.__name__}()')
        start = timer()
        func(*args, **kwargs)
        elapsed_sec = timer() - start
        print(f'---> Done {func.__name__}(): {elapsed_sec:.3f} secs')
    return inner

def sleeper(seconds: int = 0):
    print('Going to sleep...')
    sleep(seconds)
    print('Done!')


measure()是一個函式,它接收一個函式func()作為引數,並返回一個在內部宣告的函式inner()。inner()接收任何傳入的引數並將它們傳給func(),但將這個呼叫包裹在幾行中,以測量並列印以秒為單位的經過時間。 sleeper()是一個測試函式,它明確地睡了一會兒,所以我們可以測量它。

鑑於這些,我們可以構建一個測量的sleeper()函式,如:

measured_sleeper = measure(sleeper)
measured_sleeper(3)
輸出:

---> Calling sleeper()
Going to sleep...
Done!
---> Done sleeper(): 3.000 secs


這很有效,但如果我們已經在很多地方使用了sleeper(),我們就必須用measured_sleeper()來取代所有這些呼叫。相反,我們可以

sleeper = measure(sleeper)

這裡我們替換了當前作用域中的sleeper引用,使之指向原始sleeper()函式的測量版本。這和在函式宣告前面加上@decorator 是一回事。

@measure
def sleeper(seconds: int = 0):
    print('Going to sleep...')
    sleep(seconds)
    print('Done!')


因此,@decorators只是語法上的糖,將一個新定義的函式傳遞給一個現有的裝飾器函式,該函式返回一個新的函式,並讓原來的函式名指向這個新的函式。

@repeat:引數化的裝飾器函式
在上面的例子中,我們採用了一個現有的函式sleeper(),並用一個函式的取值和返回函式的措施()來裝飾它,即一個@裝飾器。如果我們想把引數傳遞給裝飾器函式本身呢?例如,設想我們有一個函式,我們想重複它n次。為了達到這個目的,我們只需要再新增一個內部函式。

def repeat(n: int = 1):
    def decorator(func: Callable):
        def inner(*args, **kwargs):
            for _ in range(n):
                func(*args, **kwargs)
        return inner
    return decorator

@repeat(n=3)
def hello(name: str):
    print(f'Hello {name}!')

hello('world')


輸出:

Hello world!
Hello world!
Hello world!


@trace: 用一個函式來裝飾一個類
我們還可以對類進行裝飾,而不僅僅是函式。舉個例子,假設我們有一個現有的類Foo,我們想對它進行追蹤,也就是說,在每次呼叫某個方法時得到一個print(),而不需要手動改變每個方法。所以我們希望能夠在類的定義前加上@trace,這樣就可以免費獲得這個功能,比如:

@trace
class Foo:
    i: int = 0
    def __init__(self, i: int = 0):
        self.i = i
    def increment(self):
        self.i += 1
    def __str__(self):
        return f'This is a {self.__class__.__name__} object with i = {self.i}'



trace()是什麼樣子的?它必須接受一個cls引數(新定義的類,在我們的例子中是Foo),並返回一個新的/修改過的類(加入了跟蹤功能)。

def trace(cls: type):
    def make_traced(cls: type, method_name: str, method: Callable):
        def traced_method(*args, **kwargs):
            print(f'Executing {cls.__name__}::{method_name}...')
            return method(*args, **kwargs)
        return traced_method
    for method_name, method in getmembers(cls, ismethod):
        setattr(cls, method_name, make_traced(cls, method_name, method))
    return cls



這個實現是非常直接的。我們遍歷 cls.__dict__.items() 中的所有方法,並用一個包裝好的方法來替換,我們用內部的 make_traced() 函式製造這個方法。它是有效的。

f1 = Foo()
f2 = Foo(4)
f1.increment()
print(f1)
print(f2)

輸出:

Executing Foo::__init__...
Executing Foo::__init__...
Executing Foo::increment...
Executing Foo::__str__...
This is a Foo object with i = 1
Executing Foo::__str__...
This is a Foo object with i = 4



@singleton:單例模式
用函式來裝飾一個類的第二個例子是實現常見的單子模式。

在軟體工程中,單子模式是一種軟體設計模式,它將一個類的例項化限制為一個 "單一 "的例項。當完全需要一個物件來協調整個系統的行動時,這很有用。

我們的實現是一個Python裝飾器@singleton:

def singleton(cls: type):
    def __new__singleton(cls: type, *args, **kwargs):
        if not hasattr(cls, '__singleton'):
            cls.__singleton = object.__new__(cls) # type: ignore
        return cls.__singleton                    # type: ignore
    cls.__new__ = __new__singleton                # type: ignore
    return cls


正如在Enum文章中提到的,在呼叫__init__()對新建立的例項進行初始化之前,會呼叫__new__()類方法來構造新的物件。所以,為了得到單子行為,我們只需要覆蓋__new__(),使其總是返回一個單一的例項。讓我們來測試一下。

@singleton
class Foo:
    i: int = 0
    def __init__(self, i: int = 0):
        self.i = i
    def increment(self):
        self.i += 1
    def __str__(self):
        return f'This is a {self.__class__.__name__} object with i = {self.i}'

@singleton
class Bar:
    i: int = 0
    def __init__(self, i: int = 0):
        self.i = i
    def increment(self):
        self.i += 1
    def __str__(self):
        return f'This is a {self.__class__.__name__} object with i = {self.i}'

f1 = Foo()
f2 = Foo(4)
f1.increment()
b1 = Bar(9)
print(f1)
print(f2)
print(b1)
print(f1 is f2)
print(f1 is b1)

輸出:

This is a Foo object with i = 5
This is a Foo object with i = 5
This is a Bar object with i = 9
True
False


@Count:用一個類來裝飾一個類
上面的程式碼之所以有效,是因為在 Python 中,類的宣告實際上只是一個函式的語法糖,這個函式構造一個新的型別物件。例如,上面宣告的類 Foo 也可以透過程式設計方式來定義,比如:

def make_class(name):
    cls = type(name, (), {})
    setattr(cls, 'i', 0)
    def __init__(self, i): self.i = i
    setattr(cls, '__init__', __init__)
    def increment(self): self.i += 1
    setattr(cls, 'increment', increment)
    def __str__(self): return f'This is a {self.__class__.__name__} object with i = {self.i}'
    setattr(cls, '__str__', __str__)
    return cls

Foo = make_class('Foo')


但是,如果是這樣的話,我們不僅可以用函式來裝飾一個函式,用函式來裝飾一個類,還可以用類來裝飾一個類。讓我們看一個@Count模式的例子,我們想計算建立的例項的數量。我們有一個現有的類,我們希望能夠在類的定義前加上@Count,然後得到一個 "免費 "的建立例項的數量,然後我們可以使用裝飾器Count類來訪問。解決辦法是:

class Count:
    instances: DefaultDict[str, int] = defaultdict(int) # we will use this as a class instance
    def __call__(self, cls): # here cls is either Foo or Bar
        class Counted(cls): # here cls is either Foo or Bar
            def __new__(cls: type, *args, **kwargs): # here cls is Counted
                Count.instances[cls.__bases__[0].__name__] += 1
                return super().__new__(cls) # type: ignore
        Counted.__name__ = cls.__name__
        # without this ^ , self.__class__.__name__ would
        # be 'Counted' in the __str__() functions below
        return Counted



訣竅在於,當一個類被用Count裝飾時,它的__call__()方法會被執行時呼叫,並且該類被作為cls傳入。在內部,我們構造了一個新的類 Counted,它的父類是 cls,但重寫了 __new__(),並在 Count 類的變數例項中增加了一個計數器(但除此之外還建立了一個新的例項並返回)。然後,新構造的 Counted 類(其名稱被重寫)被返回,並取代了原來定義的類。讓我們看看它的操作。

@Count()
class Foo:
    i: int = 0
    def __init__(self, i: int = 0):
        self.i = i
    def increment(self):
        self.i += 1
    def __str__(self):
        return f'This is a {self.__class__.__name__} object with i = {self.i}'
@Count()
class Bar:
    i: int = 0
    def __init__(self, i: int = 0):
        self.i = i
    def increment(self):
        self.i += 1
    def __str__(self):
        return f'This is a {self.__class__.__name__} object with i = {self.i}'

f1 = Foo()
f2 = Foo(6)
f2.increment()
b1 = Bar(9)
print(f1)
print(f2)
print(b1)
for class_name, num_instances in Count.instances.items():
    print(f'{class_name} -> {num_instances}')

輸出:
This is a Foo object with i = 0
This is a Foo object with i = 7
This is a Bar object with i = 9
Foo -> 2
Bar -> 1



@app.route: 透過裝飾函式構建類似Flask的應用物件
最後,我們中的許多人都使用過Flask,並按照以下的思路編寫過HTTP處理函式:

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
    return 'Hello, World!'


這是對裝飾器模式的又一次創造性使用。在這裡,我們透過新增我們的自定義處理函式來建立一個應用物件,但我們不必擔心定義我們自己的從Flask派生的類,我們只需編寫我們裝飾的平面函式。這個功能可以直接複製成一個玩具Router類。

class Router:
    routes: dict[str, Callable] = {}

    def route(self, prefix: str):
        def decorator(func: Callable):
            self.routes[prefix] = func
        return decorator

    def default_handler(self, path):
        return f'404 (path was {path})'

    def handle_request(self, path):
        longest_match, handler_func = 0, None
        for prefix, func in self.routes.items():
            if path.startswith(prefix) and len(prefix) > longest_match:
                longest_match, handler_func = len(prefix), func
        if handler_func is None:
            handler_func = self.default_handler
        print(f'Response: {handler_func(path)}')


這裡唯一的訣竅是Router::route()可以像一個裝飾器一樣行事,並返回一個函式。使用例項:

app = Router()

@app.route('/')
def hello(_):
    return 'Hello to my server!'

@app.route('/version')
def version(_):
    return 'Version 0.1'

app.handle_request('/')
app.handle_request('/version')
app.handle_request('does-not-exist')
輸出:

Response: Hello to my server!
Response: Version 0.1
Response: 404 (path was does-not-exist)


@decorator vs @decorator()
在@measure的例子中,我們寫道。

@measure
def sleeper(seconds: int = 0):
    ...



我們是否也可以在def前寫上@measure()?不可以! 我們會得到一個錯誤:

measure() missing 1 required positional argument: 'func'


但是,在app.route()的例子中,我們確實寫了()的括號。
很簡單:@decorator def func被func = decorator(func)所取代。
如果我們寫@decorator() def func,它就會被func = decorator()(func)取代。
所以在後一種情況下,decorator()被執行,它需要返回一個接受一個函式作為引數的函式,並返回一個函式。

這就是所有裝飾器接受一個引數的例子的結構方式。



 

相關文章