裝飾器詳解
閉包
要想理解裝飾器,首先得弄明白什么是閉包
函數定義和函數表達式位於另一個函數的函數體內。而且,這些內部函數可以訪問它們所在的外部函數中聲明的所有局部變量、參數和聲明的其他內部函數。當其中一個這樣的內部函數在包含它們的外部函數之外被調用時,就會形成閉包
def wrapper():
name = "ivy"
def inner():
print(name)
return inner
g = wrapper()
g()
根據上面的定義,wrapper函數里面定義了inner函數,inner函數里面使用了wrapper中的name變量。wrapper函數被調用后會返回內部的inner函數給g,當g再次被調用時,就會形成閉包。
為什么要使用裝飾器
需求: 在不修改調用代碼的情況下,計算一段業務代碼的耗時
import time
def your_work():
start = time.time()
time.sleep(3) # 模仿業務耗時
end = time.time()
print(end - start)
your_work()
這里的time.sleep模擬任務的耗時,從這段代碼中可以看到,計時和實際的業務代碼寫在一個函數中,可讀性差,如果從面向對象的角度來看,這段代碼違反了單一職責原則。
import time
def your_work():
time.sleep(3)
def spent_time(func):
start = time.time()
func()
end = time.time()
print(end - start)
spent_time(your_work)
將上面的代碼修改,將計時和業務代碼分開,雖然可讀性高了,但是調用方式發生了改變,不符合需求。
這個時候,就要使用到裝飾器了。
裝飾器原理
import time
def wrapper(func):
print("wrapper....")
def inner():
start = time.time()
func()
end = time.time()
print(end - start)
return inner
def your_work():
time.sleep(3)
g = wrapper(your_work)
g()
分析:
* 利用上面的閉包原理,wrapper函數接受一個函數作為參數,當wrapper函數被執行的時候,實際執行print("wrapper....")和返回inner函數,因為執行wrapper函數的時候inner函數沒有被執行,所以傳進來的參數函數也沒有被執行。
* 在上述代碼中,只有當wrapper函數的接受者(inner的接受者)g被調用的時候,inner才會被執行。因為inner里面有且僅有func,也就是傳進來的your_work,所以當g被調用的時候,也就是your_work被調用的時候,這時候可以把g看做your_work
import time
def wrapper(func):
print("wrapper....")
def inner():
start = time.time()
func()
end = time.time()
print(end - start)
return inner
@wrapper
def your_work():
time.sleep(3)
your_work()
python提供了一種語法糖@,@的作用只是將所裝飾的函數(your_work)傳入裝飾器函數(wrapper)作為參數,並執行裝飾器函數。當被裝飾的函數(your_work)被調用的時候,實際上是調用的裝飾器內部返回的函數(inner), 這樣就可以完成不修改調用處代碼,又能達到計時目的。
裝飾器返回值和裝飾器參數
上述的所裝飾的函數(your_work)既沒有返回值也沒有參數,因為被裝飾的函數最后會執行裝飾器函數所返回的閉包函數(inner), 所以inner所接受的參數和返回的值就是被裝飾函數返回的值和接受的參數,因為裝飾器最初的目的是為原有的代碼增加功能而不修改原來的邏輯,所以函數的返回值和函數的參數應該和被裝飾的函數一致。
import time
def wrapper(func):
def inner(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(end - start)
return result
return inner
@wrapper
def do_something(num):
time.sleep(3)
return num
在inner里面使用不定長參數來接受任意參數,在調用參數func的函數時候將接受的參數傳遞給func,在將函數運行的結果保存,最后返回即可。
帶參數的裝飾器
from flask import Flask
app = Flask(__name__)
@app.route('/index')
def index():
return 'index page'
if __name__ == '__main__':
app.run()
熟悉flask 的應該很熟悉這段代碼,flask 使用裝飾器接受參數來定義路由。
模仿flask寫一個接受參數的裝飾器
class App:
def __init__(self):
self.urls = dict()
def route(self, url:str):
def wrapper(func):
self.urls[url] = func
return func
return wrapper
app = App()
@app.route('/index')
def index():
return 'index page'
在App.route方法中,接受一個字符串作為參數,再在內部將接受的參數func做一個映射關系存入實例屬性中。
注意
* 在app.route對index進行裝飾的時候,app.route接受了一個字符串作為參數,主動執行了route方法,返回了wrapper函數,然后@語法在使用route返回的wrapper對index進行了裝飾。所以index函數最后傳入了wrapper中而不是直接傳入route。
類裝飾器
import time
class Wrapper:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
start = time.time()
result = self.func(*args, **kwargs)
end = time.time()
print(end - start)
return result
@Wrapper
def your_work(num):
time.sleep(3)
return num
類也可以作為一個裝飾器,根據上述原理,裝飾器會將your_work傳入Wrapper,由於類直接被調用會觸發它的__init___方法,所以在實例化方法里面接受被裝飾器的函數作為實例函數屬性保存。此時被裝飾的函數可以看到裝飾器類的一個實例變量,這個函數被調用的時候(可以看成是類的實例變量被調用,此時會觸發該類的__call__方法),最后在__call__方法里面寫上自己的處理邏輯即可
一個簡單的緩存裝飾器
import time
class Cache:
def __init__(self, func):
self.func = func
self._value = None
def __call__(self, *args, **kwargs):
if self._value is None:
result = self.func(*args, **kwargs)
self._value = result
return self._value
@Cache
def your_work(num):
time.sleep(3)
return num
print(your_work(3))
print(your_work(4))
在這個例子中,假設your_work是一個耗時的任務,但是它每次返回的結果都相同,所以可以在它第一次運行完畢之后將它的結果保存起來,下次再次調用它的時候直接將結果返回,這樣就避免了重復的運算。
裝飾類
裝飾器不僅可以裝飾函數,也可以裝飾類,原理和函數一致,只用將傳遞的參數函數換成類即可!
多個裝飾器
from tornado.httpclient import AsyncHTTPClient
from tornado.web import gen
from tornado.ioloop import IOLoop
class Request:
@classmethod
@gen.coroutine
def get_resp(cls, url):
client = AsyncHTTPClient()
resp = yield client.fetch(url)
print(resp.body)
if __name__ == '__main__':
Request.get_resp('https://www.baidu.com')
IOLoop.instance().start()
熟悉tornado
的都應該熟悉這段代碼,這是tornado
實現一個異步獲取網絡響應的最基本的寫法。
當一個對象被多個裝飾器所裝飾,那么裝飾器的執行順序是怎樣的?
def test2(func):
def inner():
print("test2----before")
func()
print("test2----after")
return inner
def test3(func):
def inner():
print("test3----before")
func()
print("test3----after")
return inner
@test3
@test2
def test1():
print("test1---")
if __name__ == '__main__':
test1()
""" 運行結果
test3----before
test2----before
test1---
test2----after
test3----after
"""
洋蔥模型
從上面的運行結果可以看出,當一個對象被多個裝飾器所裝飾的時候,所有的需要在該對象本身test1
運行前而運行的順序是根據裝飾器裝飾的順序從上而下的,而所有需要在被裝飾對象test1
本身運行后所運行的順序是裝飾器裝飾順序由下而上的。
如果讀懂了上面的裝飾器原理,那么很容易理解這個例子
我們可以把這個順序看成一個洋蔥模型,先從外面一層層進去,最后從里面一層層出來。
裝飾器的基本原則
裝飾器的本意是在不修改原來的代碼和調用方式的情況下給被裝飾的對象增加新的功能,所以我們在書寫裝飾器的時候如果沒有特殊需求的話,盡量不要在裝飾器內部去修改被裝飾器對象的返回值(如果有返回值),這樣可以使用多個裝飾器來裝飾而不容易發生錯誤。
以上就是裝飾器大致的內容