天天看點

python-裝飾器

2018-12-04 10:20:29

裝飾器(Decorators)是 Python 的一個重要部分。簡單地說:他們是修改其他函數的功能的函數。他們有助于讓我們的代碼更簡短,也更Pythonic。大多數初學者不知道在哪兒使用它們,是以我将要分享下,哪些區域裡裝飾器可以讓你的代碼更簡潔。 首先,讓我們讨論下如何寫你自己的裝飾器。

裝飾器本質上是一個函數。它把一個函數作為輸入并且傳回另外一個函數。在裝飾器中,通常使用下面的這些python技巧:

  • *args和**kwargs
  • 閉包
  • 作為參數的函數

簡單的來說,裝飾器讓你在一個函數的前後去執行代碼。

一、簡單實作一個裝飾器

def a_new_decorator(a_func):
    """
    裝飾器,以函數為輸入,傳回經過修飾的函數
    """
    def wrapTheFunction():
        print("I am doing some boring work before executing a_func()")
        a_func()
        print("I am doing some boring work after executing a_func()")

    return wrapTheFunction


def a_function_requiring_decoration():
    print("I am the function which needs some decoration to remove my foul smell")


if __name__ == "__main__":
    # 添加裝飾器
    a_function_requiring_decoration = a_new_decorator(a_function_requiring_decoration)
    # 調用裝飾器
    a_function_requiring_decoration()      

這正是 python 中裝飾器做的事情!它們封裝一個函數,并且用這樣或者那樣的方式來修改它的行為。

@ 符号是一個簡短的方式來生成一個被裝飾的函數,這一點和Java中的注解注入是很類似的,這裡是我們使用 @ 來運作之前的代碼: 

def a_new_decorator(a_func):
    """
    裝飾器,以函數為輸入,傳回經過修飾的函數
    """
    def wrapTheFunction():
        print("I am doing some boring work before executing a_func()")
        a_func()
        print("I am doing some boring work after executing a_func()")

    return wrapTheFunction

@a_new_decorator
def a_function_requiring_decoration():
    """
    使用注解的方式,等價于: a_function_requiring_decoration = a_new_decorator(a_function_requiring_decoration)
    """
    print("I am the function which needs some decoration to remove my foul smell")


if __name__ == "__main__":
    a_function_requiring_decoration()        

二、functools.wraps

使用上述的兩種方式生成裝飾器函數這裡有個問題,就是在采用這種方式進行裝飾的時候,最後傳回的函數已經變成了wrapTheFunction(),那麼當我們檢視函數的說明的時候就會丢失掉原來函數的說明,如下的代碼所描述的:

print(a_function_requiring_decoration.__name__)
# Output: wrapTheFunction
      

這并不是我們想要的!Ouput輸出應該是"a_function_requiring_decoration"。這裡的函數被warpTheFunction替代了。它重寫了我們函數的名字和注釋文檔(docstring)。幸運的是Python提供給我們一個簡單的函數來解決這個問題,那就是functools.wraps。我們修改上一個例子來使用functools.wraps:

from functools import wraps


def a_new_decorator(a_func):
    """
    裝飾器,以函數為輸入,傳回經過修飾的函數
    """
    @wraps(a_func)
    def wrapTheFunction():
        print("I am doing some boring work before executing a_func()")
        a_func()
        print("I am doing some boring work after executing a_func()")

    return wrapTheFunction


@a_new_decorator
def a_function_requiring_decoration():
    """
    使用注解的方式,等價于: a_function_requiring_decoration = a_new_decorator(a_function_requiring_decoration)
    """
    print("I am the function which needs some decoration to remove my foul smell")


if __name__ == "__main__":
    a_function_requiring_decoration()
    print(a_function_requiring_decoration.__name__)      
注意:@wraps接受一個函數來進行裝飾,并加入了複制函數名稱、注釋文檔、參數清單等等的功能。這可以讓我們在裝飾器裡面通路在裝飾之前的函數的屬性。

由于func函數有可能是帶參數的函數,那麼就需要将參數一起傳給wrap函數,而@wraps(f)正好可以進行參數的複制,是以可以将func原先的參數傳遞到裝飾器函數中,由于我們事先不知道會有多少參數,以及是否含有字典類型,是以這裡的參數表最好寫成*args, **kwargs,*args會将不定長的參數以元組的形式傳入,**kwargs會将字典類型的資料傳入。

這裡提供一個使用裝飾器的藍本:

from functools import wraps


def a_new_decorator(a_func):
    """
    裝飾器,以函數為輸入,傳回經過修飾的函數
    """
    @wraps(a_func)
    def wrapTheFunction(*args, **kwargs):
        print("I am doing some boring work before executing a_func()")
        a_func(*args, **kwargs)
        print("I am doing some boring work after executing a_func()")

    return wrapTheFunction


@a_new_decorator
def a_function_requiring_decoration(a, b):
    """
    使用注解的方式,等價于: a_function_requiring_decoration = a_new_decorator(a_function_requiring_decoration)
    """
    print("I am the function which needs some decoration; calc_res = %s" % (a + b))


if __name__ == "__main__":
    a_function_requiring_decoration(1, 2)      

三、裝飾器的使用場景

  • 授權(Authorization)

裝飾器能有助于檢查某個人是否被授權去使用一個web應用的端點(endpoint)。它們被大量使用于Flask和Django web架構中。這裡是一個例子來使用基于裝飾器的授權:

from functools import wraps
 
def requires_auth(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        auth = request.authorization
        if not auth or not check_auth(auth.username, auth.password):
            authenticate()
        return f(*args, **kwargs)
    return decorated
      
  • 日志(Logging) 

日志是裝飾器運用的另一個亮點。這是個例子:

from functools import wraps
 
def logit(func):
    @wraps(func)
    def with_logging(*args, **kwargs):
        print(func.__name__ + " was called")
        return func(*args, **kwargs)
    return with_logging
 
@logit
def addition_func(x):
   """Do some math."""
   return x + x
 
 
result = addition_func(4)
# Output: addition_func was called
      

四、帶參數的裝飾器

來想想這個問題,難道@wraps不也是個裝飾器嗎?但是,它接收一個參數,就像任何普通的函數能做的那樣。那麼,為什麼我們不也那樣做呢?

那麼問題就是如何将參數傳到裝飾器中呢?我們可以再對原來的裝飾器函數進行一次封裝,通過這種方式将參數給到裝飾器。

from functools import wraps
 
def logit(logfile='out.log'):
    def logging_decorator(func):
        @wraps(func)
        def wrapped_function(*args, **kwargs):
            log_string = func.__name__ + " was called"
            print(log_string)
            # 打開logfile,并寫入内容
            with open(logfile, 'a') as opened_file:
                # 現在将日志打到指定的logfile
                opened_file.write(log_string + '\n')
            return func(*args, **kwargs)
        return wrapped_function
    return logging_decorator
 
@logit()
def myfunc1():
    pass
 
myfunc1()
# Output: myfunc1 was called
# 現在一個叫做 out.log 的檔案出現了,裡面的内容就是上面的字元串
 
@logit(logfile='func2.log')
def myfunc2():
    pass
 
myfunc2()
# Output: myfunc2 was called
# 現在一個叫做 func2.log 的檔案出現了,裡面的内容就是上面的字元串
      

原先我們直接使用@logit,代表func = logit(func),這裡我們需要給裝飾器傳入參數,使用@logit(logfile = "func.log"),代表func = logit(logfile = "func.log")(func)。

五、裝飾器類

現在我們有了能用于正式環境的logit裝飾器,但當我們的應用的某些部分還比較脆弱時,異常也許是需要更緊急關注的事情。比方說有時你隻想打日志到一個檔案。而有時你想把引起你注意的問題發送到一個email,同時也保留日志,留個記錄。這是一個使用繼承的場景,但目前為止我們隻看到過用來建構裝飾器的函數。

幸運的是,類也可以用來建構裝飾器。那我們現在以一個類而不是一個函數的方式,來重新建構logit。

from functools import wraps
 
class logit(object):
    def __init__(self, logfile='out.log'):
        self.logfile = logfile
 
    def __call__(self, func):
        @wraps(func)
        def wrapped_function(*args, **kwargs):
            log_string = func.__name__ + " was called"
            print(log_string)
            # 打開logfile并寫入
            with open(self.logfile, 'a') as opened_file:
                # 現在将日志打到指定的檔案
                opened_file.write(log_string + '\n')
            # 現在,發送一個通知
            self.notify()
            return func(*args, **kwargs)
        return wrapped_function
 
    def notify(self):
        # logit隻打日志,不做别的
        pass
      

這個實作有一個附加優勢,在于比嵌套函數的方式更加整潔,而且包裹一個函數還是使用跟以前一樣的文法:

@logit()
def myfunc1():
    pass
      
class email_logit(logit):
    '''
    一個logit的實作版本,可以在函數調用時發送email給管理者
    '''
    def __init__(self, email='[email protected]', *args, **kwargs):
        self.email = email
        super(email_logit, self).__init__(*args, **kwargs)
 
    def notify(self):
        # 發送一封email到self.email
        # 這裡就不做實作了
        pass