Python里我們經常能見到@開頭的句法,也就是人們常說的裝飾器(decorator)。裝飾器是Python非常重要的一部分,能夠產出更易於維護的代碼。這篇文章會給大家帶來裝飾器的介紹以及幾個實用的例子。

裝飾器是啥

假設我有一個func_foo函數如下。

def func_foo():
print("運行func_foo")

如果我們每次都要在函數前後插入日誌,最簡單的辦法是把代碼改成如下。

def func_foo():
print("在裝飾器前我要做這些")
print("運行func_foo")
print("裝飾器後做這些")

但是如果我們想在func_bar,func_xxx等函數中一概插入同樣的日誌呢,一個個函數去改不僅費時,以後也難以維護,於是會想到以下辦法。

def a_decorator(a_func):
def wrap_the_func():
print("在裝飾器前我要做這些")
a_func()
print("裝飾器後做這些")

return wrap_the_func

def func_foo():
print("運行func_foo")

def func_bar():
print("運行func_bar")

func_foo = a_decorator(func_foo) # func1
func_bar = a_decorator(func_bar) # func2

在上面這個例子里,如果我們需要改變插入日誌的內容,我們只需要改變wrap_the_func里的內容即可。但是美中不足的是我們需要添加func1以及func2額外兩行代碼,那麼有沒有辦法讓我們能夠更簡潔更Pythonic地表達我們在最後這兩行中所要表達的東西呢?

當然有!那不正是裝飾器嘛!在Python里,上面的例子可以寫成。

@a_decorator
def func_foo():
print("運行func_foo")

@a_decorator
def func_bar():
print("運行func_bar")

簡單吧,@a_decorator加到func_foo的效果是和func_foo = a_decorator(func_foo)一樣的,這就是裝飾器的強大之處。裝飾器可以在不修改函數或者類本身代碼的情況下給函數或者類增加額外的功能以及改變他們的用法。

了解裝飾器基本概念後,接下來給大家帶來幾個Python裝飾器在實戰中的幾個例子。

1)許可權認證

在做伺服器端的時候我們通常得檢測一個用戶是否有許可權進行某些操作,因此在某些web handlers里可能會加上額外的邏輯來檢查一下用戶許可權,比如下面這個WebHandler里,我們可以在每次函數開頭通過調用寫好的PlatformAuthenticated方法來認證一下用戶。

def PlatformAuthenticated(auth, auth_level):
if not auth or not check_auth(auth.username, auth.password):
raise Exception("BlahBlahBlah")

class WebHandler:
def get(self, request, response):
auth = request.authorization(ADMIN)
PlatformAuthenticated(auth, ADMIN)
do_something()

但是這樣寫的問題在於一旦handlers多了,亦或者是許可權分成好幾種級別的話,代碼的維護性以及可讀性都會大大下降,因為我們需要把這些額外的代碼加入到每個handler的get方法裡邊。這時候可以把PlatformAuthenticated變成一個裝飾器,再把這個裝飾器添加到相應的handlers裡邊。比如

def PlatformAuthenticated(auth_level):
class StrictPlatformAuth:
def __init__(self, handler):
self.handler = handler

def __call__(self, request, response):
auth = request.authorization(auth_level)
if not auth or not check_auth(auth.username, auth.password):
raise Exception("BlahBlahBlah")
self.handler(request, response)

return StrictPlatformAuth

class WebHandler:
@PlatformAuthenticated(ADMIN)
def get(self, request, response):
do_something()

class RobotWebHandler:
@PlatformAuthenticated(ROBOT)
def get(self, request, response):
do_something()

上面這個裝飾器相當於把get這個方法變成了

get = PlatformAuthenticated(auth_level)(get)

這樣一來,通過裝飾器,我們每次調用get也變成了調用PlatformAuthenticated里的__call__ (不熟悉__call__的朋友可以百度/google之),這樣在不需要改變每個handlers代碼本身的情況下就可以進行不同層級的許可權認證了。

2)載入緩存

在編寫應用程序的時候,我們經常會向資料庫或者數據倉庫發送請求讀取某塊數據,而由於從資料庫中讀取數據非常費時,我們會把讀過的數據載入到redis等基於內存的資料庫里。在Python里我們也可以把這一塊載入緩存的邏輯寫到一個裝飾器里,讓有需求的函數調用這個裝飾器。

下面我們就寫一個這樣的基本款裝飾器,這個裝飾器會以函數的參數作為key,返回值作為value來存入緩存里。

def cached(expires_secs):
def wrapper(func):
key = f"redis_decorator:{func.__module__}:{func.__name__}"
def execute(*args):
for arg in args:
key += str(arg)
value = redis.get(key)

if not value:
redis.set(key, value, expires_secs)
value = func(*args)
return value
return execute
return wrapper

@cached(86400)
def run_query(query_type, date):
return execute_query(query_type, date)

因為有了cached這個裝飾器,每次在跑run_query之前我們都會先檢查之前是否執行過同樣的查詢,如果執行過得話我們理論上會在redis里找到查詢的返回值,這樣就不需要再從資料庫里讀取數據了。如果沒有執行過得話我們則會從資料庫里把數據讀取下來,然後再把數據載入到redis里。

3) 插入日誌

在編程中把重要的信息log出來不僅能夠幫助自己調試,也有利於提高代碼的可維護性。假如我們要在每個Python函數里把某些通用的信息都加入到日誌當中,利用裝飾器可以省去很多的功夫。比如把下面這個logger裝飾器加入到函數中後,每次函數執行前都會先把函數的名字給log出來。

def logger(func):
@wraps(func)
def log(*args, **kwargs):
logging.info(func.__name__ + " 被調用了")
return func(*args, **kwargs)
return log

@logger
def bar():
pass

也可以把log保存在不同文件上

def logger(path):
def log_decorator(func):
@wraps(func)
def log(*args, **kwargs):
with open(path, a) as writer:
writer.write(func.__name__ + " 被調用了")
return func(*args, **kwargs)
return log
return log_decorator

@logger("/var/log/bar.txt")
def bar():
pass

總結

Python裝飾器在寫自己的庫的時候真的是必不可少的利器,其他常見的應用還有單元測試,安排協程序(coroutine),單例模式等等,以後有空再補上更多的例子。


推薦閱讀:
相关文章