裝飾器非常重要,不僅使代碼看起來更簡(jiǎn)潔優(yōu)雅,而且最大限度的復(fù)用了代碼。本文主要講解裝飾器的3種用法,分別是:
1、閉包裝飾器
2、類裝飾器
3、Python第三方模塊提供的方法
個(gè)人更推薦后面兩種,可讀性較好。這只是一個(gè)習(xí)慣而已,有人也覺得第一種更加簡(jiǎn)潔。
1. 閉包裝飾器
閉包
閉包裝飾器含有兩個(gè)名詞分別是閉包和裝飾器,初學(xué)者可能不太清楚,在這里做一個(gè)簡(jiǎn)單的說明。閉包在很多語(yǔ)言中都有這個(gè)概念,是指外層函數(shù)的局部變量是內(nèi)層函數(shù)的全局變量(概念比較狹義,方便理解Python中的閉包)
我們來看這樣一個(gè)問題:
def add_tag(tag):
def add_content(content):
return "<%s> %s </%s>" % (tag, content, tag)
return add_content
>>>a = add_tag("a")
>>>a(content="hello world")
<a> hello world </a>
tag這個(gè)變量對(duì)于外層函數(shù)add_tag來說只是一個(gè)局部變量,但對(duì)與內(nèi)層函數(shù)add_content來說就是全局變量了,并且對(duì)封裝函數(shù)中任何一個(gè)變量,外部都無法訪問,這就形成了一個(gè)封閉的概念。
簡(jiǎn)單閉包裝飾器
做一個(gè)輸入檢查的問題,驗(yàn)證name=admin,password=123456。當(dāng)然如果對(duì)于多個(gè)函數(shù)接口都需要驗(yàn)證這樣的參數(shù),使用裝飾器是最好的選擇。
def login(name, password):
if name != "admin" or password != "123456":
return "username or password error"
print "login success."
print "do something."
使用裝飾器以后,登陸函數(shù)中間不必做驗(yàn)證,只需要實(shí)現(xiàn)邏輯:
def check_login(func):
def _wrapper(name, password):
if name != "admin" or password != "123456":
print "login failed"
return "username or password error"
return func(name, password)
return _wrapper
@check_login
def login(name, password):
print "login success."
print "do something."
>>>login("admin", "123456")
login success.
do something.
>>>login("admin", "156")
login failed
注意觀察外層接收函數(shù)對(duì)象,內(nèi)層接受參數(shù),那么裝飾就是這樣的
>>>check_login(login)("admin", "123456")
login success.
do something.
>>>check_login(login)("admin", "1256")
login failed
發(fā)現(xiàn)結(jié)果是一樣的,check_login(login)返回的是內(nèi)層函數(shù)對(duì)象,內(nèi)層函數(shù)剛好接收用戶名和密碼兩個(gè)參數(shù),也就是說@這個(gè)語(yǔ)法糖只是簡(jiǎn)化了展開式。
思考
仔細(xì)思考會(huì)發(fā)現(xiàn),這里有問題,這個(gè)登陸檢測(cè)裝飾器只接受兩個(gè)參數(shù),如何支持任意多個(gè)參數(shù)呢,這個(gè)問題還是留給讀者吧。
為了加深理解,再加一個(gè)日志裝飾器,輸出調(diào)用函數(shù)的名稱,兩個(gè)裝飾器一起裝飾login函數(shù)。(此處有坑,小心)
def log(func):
def _wrapper(*args, **kwargs):
print "call %s " % func.__name__
return func(*args, **kwargs)
return _wrapper
def check_login(func):
def _wrapper(name, password):
if name != "admin" or password != "123456":
print "login failed"
return "username or password error"
return func(name, password)
return _wrapper
@log
@check_login
def login(name, password):
print "login success."
print "do something."
>>>login("admin", "156")
call _wrapper
login failed
>>>login("admin", "123456")
call _wrapper
login success.
do something.
裝飾器的最外層函數(shù)接收的參數(shù)為函數(shù)名,這樣函數(shù)名就作為內(nèi)部的全局變量,在任何地方都可以調(diào)用了,內(nèi)層函數(shù)接受函數(shù)的參數(shù),主要的邏輯放在內(nèi)層函數(shù)里面,比如添加日志信息,或者做參數(shù)檢查等操作。最后就是按層級(jí)關(guān)系返回函數(shù),但在這里還有一個(gè)問題,就是調(diào)用函數(shù)的名稱發(fā)生了變化,因?yàn)橥鈱雍瘮?shù)是由_wrapper返回的,因此最終得到的函數(shù)簽名就變成了_wrapper。
只需要導(dǎo)入functools下的wraps,然后裝飾在內(nèi)層函數(shù)上就能保證函數(shù)簽名的了。
import functools
def log(func):
@functools.wraps(func)
def _wrapper(*args, **kwargs):
print "call %s " % func.__name__
return func(*args, **kwargs)
return _wrapper
給裝飾器添加參數(shù)
不僅需要輸出函數(shù)名的調(diào)用,還需要指定日志的級(jí)別,比如我們裝飾一個(gè)函數(shù)@log(info),這個(gè)函數(shù)就得輸出[info] call login。只需要在最外層改變接受參數(shù),次內(nèi)層接受函數(shù)名,內(nèi)層接受函數(shù)的參數(shù),也就是加了三層,看起來比較復(fù)雜了。
import functools
def log(level):
def _decorator(func):
@functools.wraps(func)
def _wrapper(*args, **kwargs):
print "[%s] call %s " % (level, func.__name__)
return func(*args, **kwargs)
return _wrapper
return _decorator
@log("info")
def login(name, password):
print "login success."
print "do something."
>>>login("admin", "123456")
[info] call login
login success.
do something.
對(duì)于三層的裝飾器,@語(yǔ)法糖相當(dāng)于log("info")(login)("admin", "123456"),看著是不是有點(diǎn)蒙了,(我剛開始學(xué)習(xí)的時(shí)候也是抓狂的,所以推薦后面兩種),理解以后用哪種就看個(gè)人口味了。給出部分代碼如下:
def login(name, password):
print "login success."
print "do something."
>>>log("info")(login)("admin", "123456")
[info] call login
login success.
do something.
2. 類裝飾器
最麻煩的閉包裝飾器解決了,類裝飾器寫起來容易,并且可讀性好多了,廢話不多說,直接實(shí)現(xiàn)上面日志例子吧。
class Log(object):
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
print "call %s" % self.func.__name__
return self.func(*args, **kwargs)
@Log
def login(name, password):
print name, password
>>> login("admin", "123456")
call login
admin 123456
初始化返回的對(duì)象必須是可調(diào)用的,只需要改變類的__call__屬性,代碼是不是易懂多了。實(shí)現(xiàn)帶參數(shù)裝飾器,也就是添加日志的級(jí)別的裝飾器,稍稍麻煩一點(diǎn):
class Log(object):
def __init__(self, level):
self.level = level
def __call__(self, func):
def _wrapper(*args, **kwargs):
print "[%s] call %s" % (self.level, func.__name__)
return func(*args, **kwargs)
return _wrapper
@Log("debug")
def login(name, password):
print name, password
>>> login("admin", "123456")
[debug] call login
admin 123456
3.第三方包實(shí)現(xiàn)的裝飾器
wrapt這個(gè)包封裝了裝飾器,直接使用@wrapt.decorator裝飾在函數(shù)上即可,對(duì)于無參數(shù)裝飾器,實(shí)現(xiàn)很簡(jiǎn)單了,可讀性就不用多說了,畢竟是第三方的包。
import wrapt
@wrapt.decorator
def log(func, instance, args, kwargs):
print "call %s" % func.__name__
return func(*args, **kwargs)
@log
def login(name, password):
print name, password
>>>login("admin", "123456")
call login
admin 123456
有參數(shù)的情況,還需要在外面加一層函數(shù),這層函數(shù)用來接收參數(shù)。
import wrapt
def log(level):
@wrapt.decorator
def wrapper(func, instance, args, kwargs):
print "[%s]: call %s" % (level, func.__name__)
return func(*args, **kwargs)
return wrapper
@Log("debug")
def login(name, password):
print name, password
>>>login("admin", "123456")
[debug] call login
admin 123456
小結(jié)
裝飾器非常重要,剛開始學(xué)習(xí)都是很困難的。當(dāng)然如果你要繞過困難,不學(xué)也可以實(shí)現(xiàn)相同的功能,只是你的代碼會(huì)很糟糕,可讀性差,代碼復(fù)用率低。Python就是簡(jiǎn)潔優(yōu)雅的,你非要寫出冗余代碼,真心浪費(fèi)了Guido的一片苦心,一個(gè)上午就寫了這么點(diǎn)文章,后期還會(huì)繼續(xù)完善。