程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式師世界 >> 編程語言 >  >> 更多編程語言 >> Python

Python的裝飾器

編輯:Python

初看這個@wrap語法,不知所雲,讓我們仔細分析一下

一、背景

我們先來看下面的例子,現有一個求和函數add,現在要求統計函數執行的時長

def add(a, b):
print(a+b)

比較容易想到的是

import time
def add(a, b):
start = time.time() # 開始時間
print(a + b)
time.sleep(4) # 模擬耗時操作
long = time.time() - start # 計算時間間隔
print(f'共耗時{long}秒')

這樣做可以實現需求,但是對原函數做了修改,不僅增加了耦合性,擴展和復用也變得難以實現。

假如再增加一個記錄日志的功能,以及對程序中所有的函數都進行時長統計,想想就可怕。

 所以,我們把add作為參數,就可以這樣寫:

import time
def timer(func,*args):
start = time.time()
func(*args)
time.sleep(4) # 模擬耗時操作
long = time.time() - start
print(f'共耗時{long}秒')
timer(add,1,2)

 這樣沒有改變原函數吧?是的,但是改變了函數調用方式,每個調用add的地方都需要修改,這麼做只是轉嫁了矛盾而已。

不能修改原函數,又不能改變調用方式,那該怎麼辦呢?裝飾器是時候登場了。

二、裝飾器

在不改變原有功能代碼的基礎上,添加額外的功能。看一個例子

import time
def timer(func):
def wrapper(*args, **kwargs):
start = time.time()
func(*args, **kwargs) # 此處獲取到被裝飾的函數——func,並執行該函數
time.sleep(2)#模擬耗時操作
long = time.time() - start
print(f'共耗時{long}秒。')
return wrapper # 返回內層函數的引用
@timer
def add(a, b):
print(a+b)
add(1, 2) # 正常調用add

打印

 timer被我們改造成了裝飾器,它接受被裝飾函數為入參,返回內部嵌套函數的引用(注意:此處並未執行函數),內部的嵌套函數wrapper持有被裝飾函數的引用即func。

對著代碼觀察一下就很好的理解了,裝飾器就是類似於C++的函數指針,C#的委托。而“@”是Python的語法糖,它的作用類似於:

add = timer(add) #此處返回的是timer.<locals>.wrapper的函數引用
add(1, 2)

總結一下add函數的執行流程:

模塊加載 --> 遇到@,執行timer函數,傳入add函數-->生成timer.<locals>.wrapper函數並命名為add--> 調用add(1, 2)--> 去執行timer.<locals>.wrapper(1, 2) --> wrapper內部持有原add函數引用(func),調用func(1, 2) -->繼續執行完wrapper函數

問:如果存在多個裝飾器,執行順序是什麼樣的呢?看下面的代碼

import time
def test1(func):
def wrapper(*args, **kwargs):
print('before test1 ...')
func(*args, **kwargs)
print('after test1 ...')
return wrapper #返回內層函數的引用
def test2(func):
def wrapper(*args, **kwargs):
print('before test2 ...')
func(*args, **kwargs)
print('after test2 ...')
return wrapper #返回內層函數的引用
@test2
@test1
def add(a, b):
print(a+b)
add(1, 2) #正常調用add

 打印

可以把裝飾器想象成洋蔥,由近及遠對函數進行層層包裹,執行的時候就是拿一把刀從一側開始切,直到切到另一側結束。

三、帶參數的裝飾器

理解了裝飾器之後,我們可以思考一下,帶參數的裝飾器該怎麼寫呢?我們知道裝飾器最終返回的是嵌套函數的引用,只要記住這點,裝飾器就任由我們發揮了。寫一個帶參數的裝飾器:

 3.1不使用@wraps裝飾器

import time
def auth(permission):
def _auth(func):
def wrapper(*args, **kwargs):
print(f"驗證權限[{permission}]...")
func(*args, **kwargs)
print("執行完畢...")
return wrapper
return _auth
@auth("add")
def add(a, b):
print(a + b)
add(2,3)
print(add.__name__)
print(add.__doc__)

注:不使用@wraps裝飾器時候,看看__name__、__doc__輸出的內容是什麼 

 

 我們可以看到,返回的是wrapper方法的引用,也就是讓add指向了wrapper方法,所以調用add.__name__, 實際上是wrapper.__name__,這樣子可能會造成後面查找該方法的名字與注釋時候會得到裝飾器的內嵌函數的名字和注釋。

3.2 使用@wraps(func)

import time
from functools import wraps
def auth(permission):
def _auth(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"驗證權限[{permission}]...")
func(*args, **kwargs)
print("執行完畢...")
return wrapper
return _auth
@auth("add")
def add(a, b):
print(a + b)
print(add(2,3))
print(add.__name__)
print(add.__doc__)

總結:Python裝飾器(decorator)在實現的時候,被裝飾後的函數其實已經是另外一個函數了(函數名等函數屬性會發生改變),為了不影響,Python的functools包中提供了一個叫wraps的decorator來消除這樣的副作用。寫一個decorator的時候,最好在實現之前加上functools的wrap,它能保留原有函數的名稱和docstring。

參考:

python裝飾器的wraps作用_hqzxsc2006的博客-CSDN博客_python3 wraps

 Python裝飾器深度解析 - 知乎


  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式師世界 All Rights Reserved