深刻理解 Python 裝飾器

1.介紹

Python裝飾器在開發過程當中,有着較爲重要的地位,可是對於初學者來講,並不便於理解,本文將帶着你們分析python裝飾器的使用。javascript

2.定義

裝飾器本質上就是一個函數,這個函數接受其餘函數做爲參數,並將其以一個新的修改後的函數做爲替換。
概念較爲抽象,咱們來考慮以下一個場景,如今咱們須要對用戶年齡進行認證,若是年齡小於18,則給出提示,年齡不符合要求(嘿嘿嘿,你們都懂)。代碼以下:java

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複製代碼

考慮到複用性的問題,咱們對其修改:python

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複製代碼

如今,代碼看起來整潔了一點,可是用裝飾器的話能夠作的更好:app

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 裝飾器的本質

上面說到裝飾器的本質就是一個函數,這個函數接受另外一個函數做爲參數,並將其其以一個新的修改後的函數進行替換。再來看下面一個例子,能夠幫咱們更好的理解:spa

def bread(func):
    def wrapper():
        print ("</''''''\>")
        func()
        print ("</______\>")
    return wrapper

def sandwich():
    print('- sandwich -')

sandwich_copy = bread(sandwich)
sandwich_copy()複製代碼

輸出結果以下:設計

</''''''\>
- sandwich -
</______\>複製代碼

bread是一個函數,它接受一個函數做爲參數,而後返回一個新的函數,新的函數對原來的函數進行了一些修改和擴展(打印一些東西),且這個新函數能夠當作普通函數進行調用。
使用python提供的裝飾器語法,簡化上面的代碼:code

def bread(func):
    def wrapper():
        print ("</''''''\>")
        func()
        print ("</______\>")
    return wrapper

@bread
def sandwich():
    print('- sandwich -')

sandwich =  sandwich()複製代碼

到這裏,咱們應該理解了裝飾器的用法和做用了,再次強調一遍,裝飾器本質上就是一個函數,這個函數接受其餘的函數做爲參數,並將其以一個新的修改後的函數進行替換ip

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="" 0x10be19320="">, '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.總結

到這裏,關於裝飾器的理解,咱們就介紹完了,配合在實際開發中的使用,你很快就能掌握它。

相關文章
相關標籤/搜索