Python 元編程 – 裝飾器
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
函數,自然函數的元資訊肯定不再是原函數的內容。
對於一個函數被多個裝飾器修飾的情況:
- 在包裝時,採用就近原則,從近點開始包裝。
- 在被調用時,採用就遠原則,從遠點開始執行。
這自然也符合棧的調用過程。
參考
//www.programiz.com/python-programming/decorator