Python装饰器学习笔记
Python裝飾器
文章目錄
- Python裝飾器
- 基本概念
- 從零開始的逐步分析
- 修飾后的問題
- 向被包裝后的函數傳遞參數
- 使用場景:stdout日志
- 接受參數的裝飾器
- 作為一個類的裝飾器
- 總結
學習資料:
- Python 函數裝飾器
- 裝飾器類
- Python __call__()方法(詳解版)
基本概念
先建立一個觀點:
函數本質上是一個對象,可以賦值給其他變量
加上()后就變成執行函數本身,并獲得其返回值
修改其他函數功能的函數,即是裝飾器
因為它要修改其他函數,所以裝飾器本身應該返回一個函數
從而將這個修改后的函數賦值給原函數,以達到修改原函數功能的目的
從零開始的逐步分析
首先是一個原型理解
主要理解函數本身是一個對象
可以賦值給其他變量,或是本身作為參數
上述代碼的函數hi不能接受參數
即如果變成這樣子,則會報錯:
輸出:
hi Archeri Traceback (most recent call last):File "D:\python_work\guide\deco.py", line 8, in <module>deco_func(hi("Archeri"))File "D:\python_work\guide\deco.py", line 2, in deco_funcprint("doing", func.__name__) AttributeError: 'NoneType' object has no attribute '__name__'. Did you mean: '__ne__'?上面的代碼,會先執行hi("Archeri"),然后執行deco_func函數
因為hi函數加上了()因此變成了執行函數本身,并取得其返回值
由于hi函數沒有任何返回值,或者說返回NoneType
因此報錯信息最后面提醒說
'NoneType' object has no attribute '__name__'.
合情合理
為了可以給內函數傳遞參數,則可能需要在外函數里定義一個新的函數去返回
簡單的剖析代碼:
# 裝飾器,就是一個函數,接收要修飾的原函數 # 這個deco_func整體就是一個裝飾器 def deco_func(func):# 因為要返回一個函數,所以在函數里面定義一個新的函數來返回# 這個函數用于在執行原函數前后,執行一些操作def wrap_func():print("doing", func.__name__)func()# 返回“修改”了原函數功能的,內部新定義的函數return wrap_func# 原函數 def hi():print("hi")# 執行原函數,看看原功能 hi() #outputs: hi# ↓↓↓裝飾器起作用的原理就在這里↓↓↓ # 裝飾好原函數之后,將修改后的函數返回給原函數 hi = deco_func(hi) #now hi is wrapped by wrap_func()# 檢驗裝飾結果 hi() #outputs:doing hi # hi使用“@”的等效裝飾效果:
# 裝飾器,就是一個函數,接收要修飾的原函數 # 這個deco_func整體就是一個裝飾器 def deco_func(func):# 因為要返回一個函數,所以在函數里面定義一個新的函數來返回# 這個函數用于在執行原函數前后,執行一些操作def wrap_func():print("doing", func.__name__)func()# 返回“修改”了原函數功能的,內部新定義的函數return wrap_func# 原函數,使用“@”裝飾 @deco_func def hi():print("hi")# 檢驗裝飾結果 hi() #outputs:doing hi # hi# 函數定義時使用“@”裝飾 # 實際上就是下面這個展示裝飾原理的語句: # hi = deco_func(hi)以上就是裝飾器最基本的理解
修飾后的問題
但是上面使用“@”修飾函數之后
print(hi.__name__)會輸出wrap_func
即hi函數的一些屬性變了
是因為此時hi函數相當于承接了deco_func的返回值,相當于是wrap_func函數
因此hi此時更像是一個“變量”而不是“函數”
hi.__name__輸出的指代返回的wrap_func
Python為上面的問題提供了一個原生又簡單的解決方法:
from functools import wrapsdef deco_func(func):@wraps(func)def wrap_func():print("doing", func.__name__)func()return wrap_func即導入wraps,然后在裝飾器返回的函數前面用它來裝飾即可
@wraps(func)
@wrap接受一個函數來進行裝飾,并加入了復制函數名稱、注釋文檔、參數列表等等的功能。
這可以讓我們在裝飾器里面訪問在裝飾之前的函數的屬性。
向被包裝后的函數傳遞參數
下面為了自己方便理解,作以下定義:
deco_func函數:裝飾器
wrap_func函數:裝飾器的返回結果,稱為包裝函數
若想給被裝飾函數傳入參數
則在包裝函數原型中添加所有可變參數,并且在傳給內部調用的被包裝函數
使用場景:stdout日志
from functools import wrapsdef logit(func):@wraps(func)def wrap_func(*args, **kw):print("doing", func.__name__)return func(*args, **kw)return wrap_func@logit def my_add(x, y):return x + y@logit def my_dec(x, y):return x - yprint(my_add(1, 1)) print(my_dec(1, 1))output:
doing my_add 2 doing my_dec 0接受參數的裝飾器
上述裝飾器雖然能裝飾不同的函數
但是“裝飾”這一行為本身是一樣的
就好像一種啤酒,能裝在任何杯子里
但裝的量都是一樣的
不能根據具體杯子的大小去變化裝入的多少
因此可以給裝飾器加上除被裝飾函數之外的參數
來使裝飾器的裝飾行為變得可變、靈活
像是上面的日志場景,只能規定日志輸出在stdout
下面就給裝飾器添加參數,能夠選擇輸出位置
一開始我是這樣寫裝飾器的
def logit(func, logfile):@wraps(func)def wrap_func(*args, **kw):with open(logfile, 'a') as log_obj:log_obj.write("doing " + func.__name__)return func(*args, **kw)return wrap_func想著直接在裝飾器原型里加上除被裝飾函數外的另一個參數
但是當使用@去裝飾my_add函數時,傳參遇到了問題
一開始@logit("addlog"),但是提示logit() missing 1 required positional argument: 'logfile',說明這個日志文件參數給到了func
然后試試@logit(logfile="addlog"),但是又提示缺失參數func
好嘛,@logit(func=my_add, logfile="addlog"),就說我my_add未定義了
說明上面那個帶參數的裝飾器的寫法是有問題的
之后看了看正確的寫法
下面就是我的分析和猜測
上面的裝飾器,用@裝飾時使用了()
是不是意味著實際的裝飾器變成了logit函數執行后的返回結果
即是wrap_func?
也就是說@logit()=@wrap_func?
看了看正確寫法,為此又加多了一層函數去封裝?
from functools import wraps# 最外層接受裝飾器的參數 def logit(logfile):# 因為使用“@”裝飾時要加圓括號傳參,所以最外層應該返回真正的裝飾器def log_deco(func):# 真正的裝飾器,返回裝飾后的原函數@wraps(func)def wrap_func(*args, **kw):with open(logfile, 'a') as log_obj:log_obj.write("doing "+func.__name__+'\n')return func(*args, **kw)return wrap_funcreturn log_deco測試:
@logit("addlog") def my_add(x, y):return x + y@logit("declog") def my_dec(x, y):return x - yprint(my_add(1, 1)) print(my_dec(1, 1))結果輸出“2”、“0”
然后也各自生成了正確的日志文件,寫入了正確的內容
作為一個類的裝飾器
上面涉及到的裝飾器,本質上都是一個函數(普通裝飾器)
而實際上裝飾器本身也可以是一個類(裝飾器類)
例如上面的不帶參數的裝飾器,可以這樣把它寫成一個類:
class logit:# 初始化工作def __init__(self, func):self.func = func# __call__方法使得類可像函數那樣去加上()調用# 類似于之前的warp函數def __call__(self, *args, **kw):print("doing", self.func.__name__)return self.func(*args, **kw)普通的裝飾方法:
my_add = logit(my_add) # 之后的my_add變成了一個logit類的實例 print(my_add(1,1)) #output: doing my_add # 2上面等價于print(logit(my_add)(1, 1))
輸出也是一樣的
使用@的裝飾方法:
@logit def my_add(x, y):return x + yprint(my_add(1,1)) # 輸出同上面一致 # 裝飾后的my_add也變成了logit類的一個實例下面是接受參數的動態裝飾器類的寫法:
from functools import wrapsclass logit:# 因為是在這個類初始化時傳入的參數# 所以應該在__init__方法里準備好裝飾器要用的參數def __init__(self, logfile):self.logfile = logfile# 因為初始化時使用()去傳入裝飾器參數# 后面需要再調用一次才能執行這個__call__# 因此在這里使用被裝飾的函數def __call__(self, func):# 返回被裝飾后的原函數# 只定義,不執行@wraps(func)def wrap_func(*args, **kw):with open(self.logfile, "a") as log_obj:log_obj.write("doing "+func.__name__+'\n')return func(*args, **kw)return wrap_func普通的裝飾方法:
def my_add(x, y):return x + y my_add = logit("addlog")(my_add) print(my_add(1, 1)) # 輸出2,生成日志文件使用@的裝飾方法:
@logit("addlog") def my_add(x, y):return x + y print(my_add(1, 1)) # 輸出2,生成日志文件總結
裝飾器類比普通裝飾器具有更好的可讀性
聽說還要知道什么閉包的概念
會更好理解一點
先放著,會用就行
總結
以上是生活随笔為你收集整理的Python装饰器学习笔记的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [NOTE] WebGoat v8.2.
- 下一篇: VS Code运行Python程序