1.介紹
Python裝飾器在開發過程中,有著較為重要的地位,但是對於初學者來說,並不便於理解,本文將帶著大家分析python裝飾器的使用。
2.定義
裝飾器本質上就是一個函式,這個函式接受其他函式作為引數,並將其以一個新的修改後的函式作為替換。
概念較為抽象,我們來考慮如下一個場景,現在我們需要對使用者年齡進行認證,如果年齡小於18,則給出提示,年齡不符合要求(嘿嘿嘿,大家都懂)。程式碼如下:
class Movie(object):
def get_movie(self,age):
if age<18:
raise Exception('使用者年齡不符合要求')
return self.movie
def set_movie(self,age,movie):
if age <18:
raise Exception('使用者年齡不符合要求')
self.movie = movie複製程式碼
考慮到複用性的問題,我們對其修改:
def check_age(age):
if age < 18:
raise Exception('使用者年齡不符合要求')
class User(object):
def get_movie(self, age):
check_age(age)
return self.movie
def set_movie(self, age, movie):
check_age(age)
self.movie = movie複製程式碼
現在,程式碼看起來整潔了一點,但是用裝飾器的話可以做的更好:
def check_age(f):
def wrapper(*args,**kwargs):
if args[1]<18:
raise Exception('使用者年齡不符合要求')
return f(*args,**kwargs)
return wrapper
class User(object):
@check_age
def get_movie(self, age):
return self.movie
@check_age
def set_movie(self, age, movie):
self.movie = movie複製程式碼
上面這段程式碼就是使用裝飾的一個典型例子,函式check_age中定義了另一個函式wrapper,並將wrapper做為返回值。這個例子很好的展示了裝飾器的語法。
2.2 裝飾器的本質
上面說到裝飾器的本質就是一個函式,這個函式接受另一個函式作為引數,並將其其以一個新的修改後的函式進行替換。再來看下面一個例子,可以幫我們更好的理解:
def bread(func):
def wrapper():
print ("</''''''\>")
func()
print ("</______\>")
return wrapper
def sandwich():
print('- sandwich -')
sandwich_copy = bread(sandwich)
sandwich_copy()複製程式碼
輸出結果如下:
</''''''\>
- sandwich -
</______\>複製程式碼
bread是一個函式,它接受一個函式作為引數,然後返回一個新的函式,新的函式對原來的函式進行了一些修改和擴充套件(列印一些東西),且這個新函式可以當做普通函式進行呼叫。
使用python提供的裝飾器語法,簡化上面的程式碼:
def bread(func):
def wrapper():
print ("</''''''\>")
func()
print ("</______\>")
return wrapper
@bread
def sandwich():
print('- sandwich -')
sandwich = sandwich()複製程式碼
到這裡,我們應該理解了裝飾器的用法和作用了,再次強調一遍,裝飾器本質上就是一個函式,這個函式接受其他的函式作為引數,並將其以一個新的修改後的函式進行替換
3.使用裝飾器需要注意的地方
前面我們介紹了裝飾器的用法,可以看出裝飾器其實很好理解,也非常簡單。但是裝飾器還有一些需要我們注意的地方
3.1 函式的屬性變化
裝飾器動態替換的新函式替換了原來的函式,但是,新函式缺少很多原函式的屬性,如docstring和函式名。
def bread(func):
def wrapper():
print ("</''''''\>")
func()
print ("</______\>")
return wrapper
@bread
def sandwich():
'''there are something'''
print('- sandwich -')
def hamberger():
'''there are something'''
print('- hamberger -')
def main():
print(sandwich.__doc__)
print(sandwich.__name__)
print(hamberger.__doc__)
print(hamberger.__name__)
main()複製程式碼
執行上面的程式,得到如下結果:
None
wrapper
there are something
hamberger複製程式碼
在上述程式碼中,定義了兩個函式sandwich和hanberger,其中sandwich使用裝飾器@bread進行了封裝,我們獲取sandwich和hanberger的docstring和函式名字,可以看到,使用了裝飾器的函式,無法正確獲取函式原有的docstring和名字,為了解決這個問題,可以使用python內建的functools模組。
def bread(func):
@functools.wrap(func)
def wrapper():
print ("</''''''\>")
func()
print ("</______\>")
return wrapper複製程式碼
我們只需要增加一行程式碼,就能正確的獲取函式的屬性。
此外,也可以像下面這樣:
import functools
def bread(func):
def wrapper():
print ("</''''''\>")
func()
print ("</______\>")
return functools.wraps(func)(wrapper)複製程式碼
不過,還是第一種方法的可讀性要更強一點。
3.2使用inspect函式來獲取函式引數
我們再來看如下一段程式碼:
def check_age(f):
@functools.wraps(f)
def wrapper(*args,**kwargs):
if kwargs.get('age')<18:
raise Exception('使用者年齡不符合要求')
return f(*args,**kwargs)
return wrapper
class User(object):
@check_age
def get_movie(self, age):
return self.movie
@check_age
def set_movie(self, age, movie):
self.movie = movie
user = User()
user.set_movie(19,'Avatar')複製程式碼
這段程式碼執行後會直接丟擲,因為我們傳入的'age'是一個位置引數,而我們卻用關鍵字引數(kwargs)獲取使用者名稱,因此。‘kwargs.get('age')’返回None,None和int型別是無法比較的,所以會丟擲異常。
為了設計一個更加智慧的裝飾器,我們需要使用python的inspect模組。如下所示:
def check_age(f):
@functools.wraps(f)
def wrapper(*args,**kwargs):
getcallargs = inspect.getcallargs(f, *args, **kwargs)
print(getcallargs)
if getcallargs.get('age')<18:
raise Exception('使用者年齡不符合要求')
return f(*args,**kwargs)
return wrapper複製程式碼
通過inspect.getcallargs,返回一個將引數名和值作為鍵值對的字典,在上述程式碼中,返回{'self': <__main__.user object="" at="">, 'age': 19, 'movie': 'Avatar'},通過這種方式,我們的裝飾器不必檢查引數username是基於位置引數還是基於關鍵字引數,而只需在字典中查詢即可。
3.3多個裝飾器的呼叫順序
在開發中,會出現對於一個函式使用兩個裝飾器進行包裝的情況,程式碼如下:
def bold(f):
def wrapper():
return "<b>"+f()+"</b>"
return wrapper
def italic(f):
def wrapper():
return "<i>"+f()+"</i>"
return wrapper
@bold
@italic
def hello():
return "hello world"
print(hello()) # <b><i>hello world</i></b>複製程式碼
分析
在前面我們提到,裝飾器就是在外層進行了封裝:@italic hello() hello = italic(hello)複製程式碼
對於兩層封裝便是:
@bold @italic hello() hello = bold(italic(hello))複製程式碼
這樣理解多個裝飾器的呼叫順序,之後就不會再有疑問了
3.4 給裝飾器傳遞引數
現在,我們的需求修改了,並不是限定為18歲了,對於不同的地區可能是20歲,也可能是16歲。那麼我們如何設計一個通用的裝飾器呢?
def check_age(age='18'):
def decorator(f):
@functools.wraps(f)
def wrapper(*args, **kwargs):
getcallargs = inspect.getcallargs(f, *args, **kwargs)
if getcallargs.get('age') < age:
raise Exception('使用者年齡不符合要求')
return f(*args, **kwargs)
return wrapper
return decorator
class User(object):
@check_age(18)
def get_movie(self, age):
return self.movie
@check_age(18)
def set_movie(self, age, movie):
check_age(age)
self.movie = movie
user = User()
user.set_movie(16,'Avatar')複製程式碼
通過上述方式,我們可以在使用裝飾器時設定age的值,而不需要修改裝飾器內的程式碼,使程式的健壯性更強,符合開閉原則。
4.總結
到這裡,關於裝飾器的理解,我們就介紹完了,配合在實際開發中的使用,你很快就能掌握它。