Python裝飾器模式
在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()被執行,它需要返回一個接受一個函式作為引數的函式,並返回一個函式。
這就是所有裝飾器接受一個引數的例子的結構方式。
相關文章
- Python設計模式-裝飾器模式Python設計模式
- 6、Python與設計模式–裝飾器模式Python設計模式
- python裝飾器2:類裝飾器Python
- 裝飾器模式(Decorator)模式
- 設計模式----裝飾器模式設計模式
- 設計模式-裝飾器模式設計模式
- [設計模式] 裝飾器模式設計模式
- Python裝飾器探究——裝飾器引數Python
- Python 裝飾器Python
- Python裝飾器Python
- 裝飾器 pythonPython
- 設計模式(八)裝飾器模式設計模式
- 設計模式之-裝飾器模式設計模式
- 設計模式之【裝飾器模式】設計模式
- 設計模式(六):裝飾器模式設計模式
- java設計模式--裝飾器模式Java設計模式
- Python 裝飾器裝飾類中的方法Python
- python的裝飾器Python
- 1.5.3 Python裝飾器Python
- Python 裝飾器(一)Python
- Python 裝飾器原理Python
- 結構型-裝飾器模式模式
- JavaDecoratorPattern(裝飾器模式)Java模式
- Go 設計模式之裝飾器模式Go設計模式
- PHP設計模式- Decorator 裝飾器模式PHP設計模式
- Java設計模式系列-裝飾器模式Java設計模式
- Java 設計模式(五)《裝飾器模式》Java設計模式
- java設計模式之裝飾器模式Java設計模式
- java設計模式-裝飾器模式(Decorator)Java設計模式
- Java設計模式12:裝飾器模式Java設計模式
- 設計模式之裝飾器模式(decorator pattern)設計模式
- 23種設計模式(三)--裝飾器模式設計模式
- 【趣味設計模式系列】之【裝飾器模式】設計模式
- 設計模式專題(七)裝飾器模式設計模式
- PHP設計模式之裝飾器模式(Decorator)PHP設計模式
- Java學設計模式之裝飾器模式Java設計模式
- 草根學Python(十六) 裝飾器(逐步演化成裝飾器)Python
- 裝飾模式模式