Python 中提供了一個叫裝飾器的特性,用於在不改變原始物件的情況下,增加新功能或行為。
這也屬於 Python "超程式設計" 的一部分,在編譯時一個物件去試圖修改另一個物件的資訊,實現 "控制一切" 目的。
本篇文章作為裝飾器的基礎篇,在閱讀後應該瞭解如下內容:
- 裝飾器的原理?
- 裝飾器如何包裹有引數的函式?
- 裝飾器本身需要引數怎麼辦?
- 被裝飾器修飾的函式還是原函式嗎,怎麼解決?
- 裝飾器巢狀時的順序?
- 裝飾器常見的應用場景?
裝飾器原理
在具體裝飾器的內容前,先來回顧下 Python 中的基本概念:
1. Python 中,一切都是物件,函式自然也不例外
python 中的物件都會在記憶體中用於屬於自己的一塊區域。在操作具體的物件時,需要通過 “變數” ,變數本身僅是一個指標,指向物件的記憶體地址。
函式作為物件的一種,自然也可以被變數引用。
def hello(name: str):
print('hello', name)
hello('Ethan')
alias_func_name = hello
alias_func_name('Michael')
# hello Ethan
# hello Michael
alias_func_name
作為函式的引用,當然也可以作為函式被使用。
2. 函式接受的引數和返回值都可以是函式
def inc(x):
return x + 1
def dec(x):
return x - 1
def operate(func, x):
result = func(x)
return result
operate(inc,3)
# 4
operate(dec,3)
# 2
這裡 operate
中接受函式作為引數,並在其內部進行呼叫。
3. 巢狀函式
def increment():
def inner_increment(number):
return 1 + number
return inner_increment()
print(increment(100)) # 101
在 increment 內部,實現對 number add 1 的操作。
回頭再來看下裝飾器的實現:
# def decorator
def decorator_func(func):
print('enter decorator..')
def wrapper():
print('Step1: enter wrapper func.')
return func()
return wrapper
# def target func
def normal_func():
print("Step2: I'm a normal function.")
# use decorator
normal_func = decorator_func(normal_func)
normal_func()
decorator_func(func)
中,引數 func
表示想要呼叫的函式,wrapper 為巢狀函式,作為裝飾器的返回值。
wrapper
內部會呼叫目標函式 func
並附加自己的行為,最後將 func
執行結果作為返回值。
究其根本,是在目標函式外部套上了一層 wrapper 函式,達到在不改變原始函式本身的情況下,增加一些功能或者行為。
通常使用時,使用 @decorator_func
來簡化呼叫過程的兩行程式碼。
將自定義呼叫裝飾器的兩行程式碼刪掉,使用常規裝飾器的寫法加在 normal_func
的定義處,但卻不呼叫 normal_func
,可以發現一個有趣的現象:
# def decorator
def decorator_func(func):
print('enter decorator..')
def wrapper():
print('Step1: enter wrapper func.')
return func()
return wrapper
# def target func
@decorator_func
def normal_func():
print("Step2: I'm a normal function.")
發現 enter decorator..
在沒有呼叫的情況下被列印到控制檯。
這就說明,此時 normal_func
已經變成了 wrapper
函式。
@decorator_func
其實隱含了 normal_func = decorator_func(normal_func)
這一行程式碼。
對帶有引數的函式使用裝飾器
假設這裡 normal_func 需要接受引數怎麼辦?
很簡單,由於是通過巢狀函式來呼叫目標函式,直接在 wrapper
中增加引數就可以了。
# def decorator
def decorator_func(func):
def wrapper(*args, **kwargs):
print('Step1: enter wrapper func.')
return func(*args, **kwargs)
return wrapper
# def target func
def normal_func(*args, **kwargs):
print("Step2: I'm a normal function.")
print(args)
print(kwargs)
# use decorator
normal_func = decorator_func(normal_func)
normal_func(1, 2, 3, name='zhang', sex='boy')
使用 *args, **kwargs
是考慮到該 decorator 可以被多個不同的函式使用,而每個函式的引數可能不同。
裝飾器本身需要引數
在裝飾器本身也需要引數時,可以將其巢狀在另一個函式中,實現引數的傳遞。
# def decorator
def decorator_with_args(*args, **kwargs):
print('Step1: enter wrapper with args func.')
print(args)
print(kwargs)
def decorator_func(func):
def wrapper(*args, **kwargs):
print('Step2: enter wrapper func.')
return func(*args, **kwargs)
return wrapper
return decorator_func
# def target func
def normal_func(*args, **kwargs):
print("Step3: I'm a normal function.")
print(args)
print(kwargs)
normal_func = decorator_with_args('first args')(normal_func)
normal_func('hello')
# use @ to replace the above three lines of code
@decorator_with_args('first args')
def normal_func(*args, **kwargs):
print("Step3: I'm a normal function.")
print(args)
print(kwargs)
來分析下 decorator_with_args
函式:
- 由於
decorator_with_args
接受了任意數量的引數,同時由於decorator_func
和wrapper
作為其內部巢狀函式,自然可以訪問其內部的作用域的變數。這樣就實現了裝飾器引數的自定義。 decorator_func
是正常的裝飾器,對目標函式的行為進行包裝。進而需要傳遞目標函式作為引數。
在使用時:
@decorator_with_args('first args')
實際上做的內容,就是 normal_func = decorator_with_args('first args')(normal_func)
的內容:
decorator_with_args('first args')
返回decorator_func
裝飾器。decorator_func
接受的正常函式物件作為引數,返回包裝的wrapper
物件。- 最後將 wrapper 函式重新命名至原來的函式,使其在呼叫時保持一致。
保留原函式資訊
在使用裝飾器時,看起來原函式並沒有被改變,但它的元資訊卻改變了 - 此時的原函式實際是包裹後的 wrapper 函式。
help(normal_func)
print(normal_func.__name__)
# wrapper(*args, **kwargs)
# wrapper
如果想要保留原函式的元資訊,可通過內建的 @functools.wraps(func)
實現:
@functools.wraps(func)
的作用是通過 update_wrapper
和 partial
將目標函式的元資訊拷貝至 wrapper 函式。
# def decorator
def decorator_with_args(*args, **kwargs):
print('Step1: enter wrapper with args func.')
print(args)
print(kwargs)
def decorator_func(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print('Step2: enter wrapper func.')
return func(*args, **kwargs)
return wrapper
return decorator_func
裝飾器巢狀
Python 支援對一個函式同時增加多個裝飾器,那麼新增的順序是怎樣的呢?
# def decorator
def decorator_func_1(func):
print('Step1: enter decorator_func_1..')
def wrapper():
print('Step2: enter wrapper1 func.')
return func()
return wrapper
def decorator_func_2(func):
print('Step1: enter decorator_func_2..')
def wrapper():
print('Step2: enter wrapper2 func.')
return func()
return wrapper
@decorator_func_2
@decorator_func_1
def noraml_func():
pass
看一下 console 的結果:
Step1: enter decorator_func_1..
Step1: enter decorator_func_2..
fun_1
在前說明, 在對原函式包裝時,採用就近原則,從下到上。
接著,呼叫 noraml_func
函式:
Step1: enter decorator_func_1..
Step1: enter decorator_func_2..
Step2: enter wrapper2 func.
Step2: enter wrapper1 func.
可以發現,wrapper2
內容在前,說明在呼叫過程中由上到下。
上面巢狀的寫法,等價於 normal_func = decorator_func_2(decorator_func_1(normal_func))
,就是正常函式的呼叫過程。
對應執行順序:
- 在定義時,先 decorator_func_1 後 decorator_func_2.
- 在呼叫時,先 decorator_func_2 後 decorator_func_1.
應用場景
日誌記錄
在一些情況下,需要對函式執行的效率進行統計或者記錄一些內容,但又不想改變函式本身的內容,這時裝飾器是一個很好的手段。
import timeit
def timer(func):
def wrapper(n):
start = timeit.default_timer()
result = func(n)
stop = timeit.default_timer()
print('Time: ', stop - start)
return result
return wrappe
作為快取
裝飾器另外很好的應用場景是充當快取,如 lru 會將函式入參和返回值作為當快取,以計算斐波那契數列為例, 當 n 值大小為 30,執行效率已經有很大差別。
def fib(n):
if n < 2:
return 1
else:
return fib(n - 1) + fib(n - 2)
@functools.lru_cache(128)
def fib_cache(n):
if n < 2:
return 1
else:
return fib_cache(n - 1) + fib_cache(n - 2)
Time: 0.2855725
Time: 3.899999999995574e-05
總結
在這一篇中,我們知道:
裝飾器的本質,就是利用 Python 中的巢狀函式的特點,將目標函式包裹在內嵌函式中,然後將巢狀函式 wrapper
作為返回值返回,從而達到修飾
原函式的目的。
而且由於返回的是 wrapper
函式,自然函式的元資訊肯定不再是原函式的內容。
對於一個函式被多個裝飾器修飾的情況:
- 在包裝時,採用就近原則,從近點開始包裝。
- 在被呼叫時,採用就遠原則,從遠點開始執行。
這自然也符合棧的呼叫過程。