深入理解 Python 裝飾器

GodZ發表於2017-05-14

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.總結

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

相關文章