Python裝飾器

裝飾器本質上是一個 Python 函數或類,它會接受一個callable對象作爲參數,然後再返回一個callable對象作爲返回值。裝飾器可以在不修改原有對象代碼的基礎上,爲對象添加額外的功能。它經常用於有切面需求的場景,比如:插入日誌、性能測試、事務處理、緩存、權限校驗等場景,有了裝飾器,我們就可以抽象出大量與函數功能本身無關的雷同代碼到裝飾器中進行重用。

簡單裝飾器

下面是一個簡單的裝飾器例子:

def log(func):	
    def wrapper(*args, **kwargs):
        print('The following function name is [%s]' % func.__name__)
        return func(*args, **kwargs)
    
    return wrapper

@log	# 無()
def test():
    print('Hi, I am test' )

test()

# Output>>>
The following function name is [test]
Hi, I am test

上例中,log 就是一個裝飾器,它接收一個函數(func)作爲參數,同時也返回一個函數(wrapper)。從運行結果可以發現,test 函數不需要做任何修改,只需在函數定義的地方加上裝飾器,@ 符號是裝飾器的語法糖,它放在函數開始定義的地方,這樣就可以省略最後一步再次賦值的操作。函數調用的時候還是和以前一樣,如果我們還有其他的類似函數,我們可以繼續調用裝飾器來修飾函數,而不用重複修改函數或者增加新的封裝。這樣,我們不但爲對象添加了額外的功能,還提高了代碼的可重複利用性。

裝飾器在 Python 中使用如此方便,主要歸因於 Python 中一切皆對象的思想,函數也能像普通的對象一樣,能作爲參數傳遞給其他函數,可以被賦值給其他變量,可以作爲返回值,可以被定義在另外一個函數內,以及閉包結構所擁有的極大威力。

繼續看下面的例子:

def log(func):
    def wrapper():
        '''This is decorator'''
        print('The following function name is [%s]' % func.__name__)
        return func(*args, **kwargs)

    return wrapper

@log
def test():
    '''This is test''' 
    print(test.__doc__)
    print('Hi, %s' % test.__name__)

test()

# Output>>>
The following function name is [test]
This is decorator
Hi, wrapper

從運行結果中看到,使用裝飾器後,test函數的函數名和註釋文檔等元信息被wrapper的元信息替換了,這顯然不是我們想要看到的。Python提供給我們一個functools.wraps函數來解決這個問題,對上面的代碼進行修改:

from functools import wraps

'''
@wraps接受一個函數來進行裝飾,並加入了複製函數名稱、註釋文檔、參數列表等等的功能。這可以讓我們在裝飾器裏面訪問在裝飾之前的函數的屬性
'''

def log(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        '''This is decorator'''
        print('>>>The following function name is %s' % func.__name__)
        return func(*args, **kwargs)

    return wrapper

@log	
def test():
    '''This is test'''
    print(test.__doc__)
    print('Hi, %s' % test.__name__)

test()

# Output>>>
The following function name is test
This is test
Hi, test

從運行結果來看,現在已經達到了我們所期望的結果。

帶參數的裝飾器

在上面的代碼中使用的@wraps裝飾器,它可以像普通函數一樣接受參數,下面來實現一個帶參數的裝飾器,用來輸出日誌文件。

from datetime import datetime
from functools import wraps
import logging

def logit(log_level='info'):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            log_file = datetime.now().strftime('%Y%m%d') + '.log'
            log_string = 'Current function is %s' % func.__name__
            if log_level == 'warning':
                logging.warning('%s is running' % func.__name__)
                with open(log_file, 'a') as f:
                    f.write((str(datetime.now()) + '\n' + '[{}]' + log_string + '\n').format(log_level))
            elif log_level == 'info':
                logging.info('%s is running' % func.__name__)
                with open(log_file, 'a') as f:
                    f.write((str(datetime.now()) + '\n' + '[{}]' + log_string + '\n').format(log_level))
            elif log_level == 'error':
                logging.debug('%s is running' % func.__name__)
                with open(log_file, 'a') as f:
                    f.write((str(datetime.now()) + '\n' + '[{}]' + log_string + '\n').format(log_level))
            else:
                raise ValueError('log level is illegal.')
            return func(*args, **kwargs)

        return wrapper

    return decorator


@logit(log_level='warning')
def test1():
    print('I am %s' % test1.__name__)


@logit(log_level='error')
def test2():
    print('I am %s' % test2.__name__)


test1()
test2()

運行代碼,會生成一個名稱爲"當前日期.log"的文件,文件內容如下:

類裝飾器

使用類裝飾器主要是依靠重寫類的__call__方法,相比函數裝飾器,類裝飾器具有靈活度大、高內聚、封裝性等優點。使在下面的代碼中,以類的方式來重新構建logit.

import logging
from datetime import datetime
from functools import wraps

class logit:
    def __init__(self, log_level='info'):
        self.log_level = log_level
    def __call__(self, func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            log_file = datetime.now().strftime('%Y%m%d') + '.log'
            log_string = 'Current function is %s' % func.__name__
            if self.log_level == 'warning':
                logging.warning('%s is running' % func.__name__)
                with open(log_file, 'a') as f:
                    f.write((str(datetime.now()) + '\n' + '[{}]' + log_string + '\n').format(self.log_level))
            elif self.log_level == 'info':
                logging.info('%s is running' % func.__name__)
                with open(log_file, 'a') as f:
                    f.write((str(datetime.now()) + '\n' + '[{}]' + log_string + '\n').format(self.log_level))
            elif self.log_level == 'error':
                logging.debug('%s is running' % func.__name__)
                with open(log_file, 'a') as f:
                    f.write((str(datetime.now()) + '\n' + '[{}]' + log_string + '\n').format(self.log_level))
            else:
                raise ValueError('log level is illegal.')
            return func(*args, **kwargs)

        return wrapper

@logit()
def test1():
    print('I am %s' % test1.__name__)

@logit(log_level='error')
def test2():
    print('I am %s' % test2.__name__)

test1()
test2()

上述實現方法比函數嵌套的方法更加整潔,並且可以非常方便的使用繼承的場景,如使用logit作爲基類,在添加一個email的功能,代碼如下:

class email_logit(logit):
    def __init__(self,email_addr = '[email protected]',*args,**kwargs):
        self.email_addr = email_addr
        super(email_logit, self).__init__(*args,**kwargs)
    def notify(self):
        # 郵件通知相關代碼,參見:https://www.jianshu.com/p/2e1486db28ca
        pass

從現在起,@email_logit 將和 @logit 產生同樣的效果,但是在打印日誌的基礎上,還會多發送一封郵件給相關用戶。

[To be continued…]

參考文檔

  1. Python裝飾器廖雪峯

  2. Python 裝飾器執行順序迷思 , Nisen

  3. 詳解Python的裝飾器一試就錯

  4. Python函數裝飾器,菜鳥教程

  5. Python裝飾器的另類用法一試就錯

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章