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