Python裝飾器

引入裝飾器

若是想在一個函數執行先後執行一些別的代碼,好比打印一點日誌用來輸出這個函數的調用狀況那應該怎麼作呢?python

#!/usr/bin/env python
# coding=utf-8

def logger(fn):									# 函數做爲參數即fn能夠爲任何參數
    def wrap(*args, **kwargs):					# 可變參數args和kwargs
        print('call {}'.format(fn.__name__))	
        ret = fn(*args, **kwargs)				# 函數調用時的參數解構
        print('{} called'.format(fn.__name__))
        return ret								# 返回函數的返回值
    return wrap

def add(x, y):
    return x + y

logger_add = logger(add)
print(logger_add.__name__)
print(logger_add)
ret = logger_add(3, 5)
print(ret)

#輸出結果:
wrap
<function logger.<locals>.wrap at 0x7fba35f4fe18>
call add
add called
8

也能夠用如下方式來實現這種效果程序員

@logger                                                                                  
def add(x, y):                                                                            
	return x + y                                                                         ret = add(3, 5)                                                                      
print(ret) 

# 輸出結果:
call add
add called
8

這就是Python裝飾器的一個簡單使用設計模式

什麼是裝飾器?

裝飾器是用於軟件設計模式的名稱。 裝飾器能夠動態地改變函數,方法或類的功能,而沒必要直接使用子類或改變被裝飾的函數的源代碼。Python裝飾器是對Python語法的一種特殊改變,它容許咱們更方便地修改函數,方法以及類。app

當咱們按照如下方式編寫代碼時:less

@logger
def add(x, y):
	...

和單獨執行下面的步驟是同樣的:ssh

def add(x, y):
	...
logger_add = logger(add)

裝飾器內部的代碼通常會建立一個新的函數,利用*args**kwargs來接受任意的參數,上述代碼中的wrap()函數就是這樣的。在這個函數內部,咱們須要調用原來的輸入函數(即被包裝的函數,它是裝飾器的輸入參數)並返回它的結果。可是也能夠添加任何想要添加的代碼,好比在上述代碼中輸出函數的調用狀況,也能夠添加計時處理等等。這個新建立的wrap函數會做爲裝飾器的結果返回,取代了原來的函數。ide

因此在Python中,裝飾器的參數是一個函數, 返回值是一個函數的函數函數

裝飾器的示例:計時處理

寫一個裝飾器,用來計算一個函數的執行時間學習

import time

def timethis(fn):
    def wrap(*args, **kwargs):
        start = time.time()
        ret = fn(*args, **kwargs)
        end = time.time()
        print(fn.__name__, end - start)
        return ret
    return wrap

若是要對add函數計時:ui

@timethis
def add(x, y):
    return x + y

ret = add(3, 5)
print(ret)

# 輸出結果
add 1.9073486328125e-06
8

若是要對sleep函數計時:

@timethis
def sleep(x):
    time.sleep(x)

sleep(3)

# 輸出結果
sleep 3.003262519836426

保存被裝飾函數的元信息

什麼是函數的元信息

好比裝飾器的名稱,裝飾器的doc等等。咱們可使用dir函數列出函數的全部元信息:dir(sleep),輸出結果以下

['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']

能夠看到有不少的元信息,咱們比較經常使用的是__name____doc__這兩個屬性\

並且__doc__屬性也就是函數的文檔信息,能夠經過help函數查看獲得

爲何要保存被裝飾函數的元信息

改寫裝飾器的應用1:計時處理中的sleep函數以下:

@timeit
def sleep(x):
    '''This function is sleep.'''
    time.sleep(x)

sleep(3)
print(sleep.__name__)
print(sleep.__doc__)

以上代碼輸出結果以下:

3.0032713413238525
wrap
None

能夠發現sleep函數的__name__是wrap,而不是sleep,而__doc__屬性爲空,而不是sleep函數的docstring。也就是說通過裝飾器裝飾事後的函數的元信息發生了改變,這時候若是程序須要函數的元信息,那麼就有問題了。

如何保存被裝飾函數的元信息

方案1:手動給被裝飾函數的元信息賦值

__name____doc__這兩個屬性爲例

import time

def timeit(fn):
    def wrap(*args, **kwargs):
        start = time.time()
        ret = fn(*args, **kwargs)
        end = time.time()
        print(end - start)
        return ret
    wrap.__doc__ = fn.__doc__	# 手動賦值__doc__信息
    wrap.__name__ = fn.__name__	# 手動賦值__name__信息
    return wrap

@timeit
def sleep(x):
    '''This function is sleep.'''
    time.sleep(x)

if __name__ == "__main__":
    sleep(3)
    # print(dir(sleep))
    print(sleep.__name__)
    print(sleep.__doc__)

輸出結果以下

3.004547119140625
sleep
This function is sleep.

能夠發現,__name____doc__這兩個屬性確實賦值成功了。

咱們能夠將元信息賦值的過程改寫爲函數,以下

import time


def copy_properties(src, dst):	# 將元信息賦值的過程改爲函數copy_properties
    dst.__name__ = src.__name__
    dst.__doc__ = src.__doc__

def timeit(fn):
    def wrap(*args, **kwargs):
        start = time.time()
        ret = fn(*args, **kwargs)
        end = time.time()
        print(end - start)
        return ret
    copy_properties(fn, wrap)	# 調用copy_properties函數修改元信息
    return wrap

@timeit
def sleep(x):
    '''This function is sleep.'''
    time.sleep(x)

if __name__ == "__main__":
    sleep(3)
    # print(dir(sleep))
    print(sleep.__name__)
    print(sleep.__doc__)

這樣修改後,一樣能夠解決問題。

繼續修改copy_properties函數,使得copy_properties能夠返回一個函數

def copy_properties(src):
    def _copy(dst):	# 內置一個_copy函數便於返回
        dst.__name__ = src.__name__
        dst.__doc__ = src.__doc__
    return _copy

def timeit(fn):
    def wrap(*args, **kwargs):
        start = time.time()
        ret = fn(*args, **kwargs)
        end = time.time()
        print(end - start)
        return ret
    copy_properties(fn)(wrap)	# 調用copy_properties函數
    return wrap

一樣能夠問題。

若是繼續修改copy_properties函數,使得_copy函數是一個裝飾器,傳入dst,返回dst,修改以下:

def copy_properties(src):	# 先固定dst,傳入src
    def _copy(dst):	# 傳入dst
        dst.__name__ = src.__name__
        dst.__doc__ = src.__doc__
        return dst	# 返回dst
    return _copy	# 返回一個裝飾器

def timeit(fn):
    @copy_properties(fn)	# 帶參數裝飾器的使用方法
    def wrap(*args, **kwargs):
        start = time.time()
        ret = fn(*args, **kwargs)
        end = time.time()
        print(end - start)
        return ret
    return wrap

copy_properties在此處返回一個帶參數的裝飾器,所以能夠直接按照裝飾器的使用方法來裝飾wrap函數,這個修改copy_properties函數的過程稱爲函數的柯里化。

方案2:使用functools庫的@wraps裝飾器

functools庫的@wraps裝飾器本質上就是copy_properties函數的高級版本:包含更多的函數元信息。首先查看wrap裝飾器的幫助信息:

import functools
help(functools.wraps)

wrap裝飾器函數的原型是:

wraps(wrapped, assigned=('module', 'name', 'qualname', 'doc', 'annotations'), updated=('dict',))

因此這個裝飾器會複製module等元信息,可是也不是全部的元信息,而且會更新dict。

使用示例以下:

import time
import functools

def timeit(fn):
    @functools.wraps(fn)	# wraps裝飾器的使用
    def wrap(*args, **kwargs):
        start = time.time()
        ret = fn(*args, **kwargs)
        end = time.time()
        print(end - start)
        return ret
    return wrap

def sleep(x):
    time.sleep(x)

print(sleep.__name__)
print(sleep.__doc__)

編寫一個帶參數的裝飾器

若是上述的timeit裝飾器,咱們須要輸出執行時間超過若干秒(好比一秒)的函數的名稱和執行時間,那麼就須要給裝飾器傳入一個參數s,表示傳入的時間間隔,默認爲1s。

咱們能夠給寫好的裝飾器外面包一個函數timeitS,時間間隔s做爲這個函數的參數傳入,而且對內層的函數可見,而後這個函數返回寫好的裝飾器。

import time
import functools


def timeitS(s):
    def timeit(fn):
        @functools.wraps(fn)
        def wrap(*args, **kwargs):
            start = time.time()
            ret = fn(*args, **kwargs)
            end = time.time()
            if end - start > s:
                print('call {} takes {}s'.format(fn.__name__, end - start))
            else:
                print('call {} takes {}s less than {}'.format(fn.__name__, end - start, s))
            return ret
        return wrap
    return timeit

@timeitS(2)
def sleep(x):
    time.sleep(x)

sleep(3)
sleep(1)

輸出結果以下:

call sleep takes 3.001342535018921s
call sleep takes 1.000471830368042s less than 2

因此,咱們能夠將帶參數的裝飾器理解爲:

  • 帶參數的裝飾器就是一個函數, 這個函數返回一個不帶參數的裝飾器

記得幫我點贊哦!

精心整理了計算機各個方向的從入門、進階、實戰的視頻課程和電子書,按照目錄合理分類,總能找到你須要的學習資料,還在等什麼?快去關注下載吧!!!

resource-introduce

念念不忘,必有迴響,小夥伴們幫我點個贊吧,很是感謝。

我是職場亮哥,YY高級軟件工程師、四年工做經驗,拒絕鹹魚爭當龍頭的斜槓程序員。

聽我說,進步多,程序人生一把梭

若是有幸能幫到你,請幫我點個【贊】,給個關注,若是能順帶評論給個鼓勵,將不勝感激。

職場亮哥文章列表:更多文章

wechat-platform-guide-attention

本人全部文章、回答都與版權保護平臺有合做,著做權歸職場亮哥全部,未經受權,轉載必究!

相關文章
相關標籤/搜索