裝飾器基礎(chǔ)

一、裝飾器概述

裝飾器(無參):

  • 它是一個函數(shù)
  • 函數(shù)作為它的形參
  • 返回值也是一個函數(shù)
  • 可以使用@function_name方式,簡化調(diào)用

此處定義不準(zhǔn)確,只是方便理解


裝飾器和高階函數(shù):裝飾器是高階函數(shù),但裝飾器是對傳入函數(shù)的功能的裝飾(功能增強)


帶參裝飾器:

  • 它是一個函數(shù)
  • 函數(shù)作為它的形參
  • 返回值是一個不帶參的裝飾器函數(shù)
  • 使用@function_name(參數(shù)列表)方式調(diào)用
  • 可以看做在裝飾器外層又加了一層函數(shù)

二、為什么需要裝飾器

2.1 在不是用裝飾器的情況下,給某個函數(shù)添加功能

來看一個需求:一個加法函數(shù),想增強它的功能,能夠輸出被調(diào)用過以及調(diào)用的參數(shù)信息。

原函數(shù):

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

增加信息輸出功能:

def add(x, y):
  print("call add, x + y") # 日志輸出到控制臺
  return x + y

上面的加法函數(shù)是完成了需求,但是有以下的缺點:

  • 打印語句的耦合太高,換句話說,我們不推薦去修改初始的add函數(shù)原始代碼
  • 加法函數(shù)屬于業(yè)務(wù)功能,而輸出信息的功能,屬于非業(yè)務(wù)功能代碼,不該放在業(yè)務(wù)函數(shù)加法中

2.2 使用高階函數(shù)給某個函數(shù)添加功能

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

def logger(func):
    print('begin') # 增強的輸出
    f = func(4,5)
    print('end') # 增強的功能
    return f

print(logger(add))

上面的代碼做到了業(yè)務(wù)代碼與功能代碼分離,但是func函數(shù)的傳參是個問題

為了解決傳參的問題,進一步改變代碼:

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

def logger(func,*args,**kwargs):
    print('begin') # 增強的輸出
    f = func(*args,**kwargs)
    print('end') # 增強的功能
    return f

print(logger(add,5,y=60))

2.3 柯里化實現(xiàn)add函數(shù)功能增強

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

def logger(fn):
    def wrapper(*args,**kwargs):
        print('begin')
        x = fn(*args,**kwargs)
        print('end')
        return x
    return wrapper

# print(logger(add)(5,y=50))        #這行代碼等價于下面兩行代碼,只是換了一種寫法而已
add = logger(add)
print(add(x=5, y=10))

2.4 裝飾器語法糖

def logger(fn):
    def wrapper(*args,**kwargs):
        print('begin')
        x = fn(*args,**kwargs)
        print('end')
        return x
    return wrapper

@logger # 等價于add = logger(add),這就是裝飾器語法
def add(x,y):
    return x + y

print(add(45,40))

@logger 就是裝飾器語法,本質(zhì)是柯里化實現(xiàn)函數(shù)功能增強。

三、文檔字符串

  • 查看Python的幫助文檔:help(function)

  • 文檔字符串Documentation Strings:幫助文檔中的一部分內(nèi)容

    • 在函數(shù)語句塊的第一行,且習(xí)慣是多行的文本,所以多使用三引號
    • 慣例是首字母大寫,第一行寫概述,空一行,第三行寫詳細(xì)描述
    • 可以使用特殊屬性__doc__訪問這個文檔字符串
image

3.1 自定義文檔字符串

def add(x,y):
    """This is a function of addition"""
    a = x+y
    return x + y

print("name = {}\ndoc = {}".format(add.__name__, add.__doc__))

print(help(add))



#以上代碼執(zhí)行結(jié)果如下:
name = add
doc = This is a function of addition
Help on function add in module __main__:

add(x, y)
    This is a function of addition

None

3.2 裝飾器的副作用

def logger(fn):
    def wrapper(*args,**kwargs):
        'I am wrapper'
        print('begin')
        x = fn(*args,**kwargs)
        print('end')
        return x
    return wrapper

@logger #add = logger(add)
def add(x,y):
    '''This is a function for add'''
    return x + y

print("name = {}\ndoc= {}".format(add.__name__, add.__doc__))      #使用裝飾器,原函數(shù)對象的屬性都被替換了,我們的需求是查看被封裝函數(shù)的屬性,如何解決?

3.3 解決裝飾器的副作用

提供一個函數(shù),copy 被裝飾函數(shù)屬性到裝飾器函數(shù)中去。

def copy_properties(src): # 柯里化
    def _copy_properties(dst):
        dst.__name__ = src.__name__
        dst.__doc__ = src.__doc__
        return dst
    return _copy_properties

def logger(fn):
    @copy_properties(fn) # wrapper = copy_properties(fn)(wrapper)
    def wrapper(*args,**kwargs): 
        'I am wrapper'
        print('begin')
        x = fn(*args,**kwargs)
        print('end')
        return x
    return wrapper

@logger #add = logger(add)
def add(x,y):
    '''This is a function for add'''
    return x + y

print("name = {}\ndoc = {}".format(add.__name__, add.__doc__))


#以上代碼執(zhí)行結(jié)果如下:
name = add
doc = This is a function for add

通過 copy_properties 函數(shù)將被包裝函數(shù)的屬性覆蓋掉包裝函數(shù)的屬性,凡是被裝飾的函數(shù)都需復(fù)制這些屬性,這個函數(shù)很通用。

而在Python中,為了解決此問題,提供了wraps修改被裝飾的doc信息。

from functools import wraps

def logger(fn):
    @wraps(fn) #其實查看wraps源碼是利用update_wrapper()實現(xiàn)的(需要有偏函數(shù)知識),但是實際開發(fā)中我們推薦使用wraps裝飾去。
    def wrapper(*args,**kwargs):
        'I am wrapper'
        print('begin')
        x = fn(*args,**kwargs)
        print('end')
        return x
    return wrapper

@logger #add = logger(add)
def add(x,y):
    '''This is a function for add'''
    return x + y

print("name = {}\ndoc = {}".format(add.__name__, add.__doc__))


#以上代碼執(zhí)行結(jié)果如下:
name = add
doc = This is a function for add

四、裝飾器分類

4.1 無參裝飾器

需求:設(shè)計裝飾器來獲取函數(shù)執(zhí)行時長。

import datetime, time

def logger(fn):
    def wrap(*args, **kwargs):
        # before 功能增強
        print('args={}, kwargs={}'.format(args, kwargs))
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs)
        # after 功能增強
        duration = datetime.datetime.now() - start
        print("function {} took {}s".format(fn.__name__, duration.total_seconds()))
        return ret
    return wrap

@logger # 相當(dāng)于add = logger(add),調(diào)用裝飾器
def add(x, y):
    print("===call add===========")
    time.sleep(2)
    return x + y

print(add(1,2))


#以上代碼輸出結(jié)果如下:
args=(1, 2), kwargs={}
===call add===========
function add took 2.000522s
3

4.2 帶參裝飾器

需求:設(shè)計裝飾器來獲取函數(shù)執(zhí)行時長,并對時長超過閾值的函數(shù)記錄一下。

import datetime, time
from functools import wraps

def logger(duration):
    def _logger(fn):
        @wraps(fn)
        def wrapper(*args, **kwargs):
            start = datetime.datetime.now()
            ret = fn(*args, **kwargs)
            delta = (datetime.datetime.now() - start).total_seconds()
            print('it\'s so slow') if delta > duration else print('it\'s so fast')
            print('function {} took {}s'.format(fn.__name__, delta))
            return ret
        return wrapper
    return _logger

@logger(2)
def add(x, y):
    print("===call add===========")
    time.sleep(2)
    return x + y

print(add(1,2))



#以上代碼執(zhí)行結(jié)果如下:
===call add===========
it's so slow
function add took 2.001955s
3

為了傳多一個參數(shù)進去裝飾器函數(shù)中,多加了一層嵌套。這樣就會先執(zhí)行 logger(2),返回_logger。這樣又回到 @_logger 無參裝飾器的情況,把被裝飾函數(shù)傳進 @_logger,函數(shù)功能將得到增強。

改進:將記錄的功能提取出來,這樣就可以通過外部提供的函數(shù)來靈活的控制輸出。

import datetime, time
from functools import wraps

# 通過 func 參數(shù),可自定義一個輸出函數(shù)來控制輸出格式
def logger(duration, func=lambda name, duration: print('function {} took {}s'.format(name, duration))):
    def _logger(fn):
        @wraps(fn)
        def wrapper(*args, **kwargs):
            start = datetime.datetime.now()
            ret = fn(*args, **kwargs)
            delta = (datetime.datetime.now() - start).total_seconds()
            print('it\'s so slow') if delta > duration else print('it\'s so fast')
            func(fn.__name__, delta) # func 參數(shù)是一個函數(shù)
            return ret
        return wrapper
    return _logger

@logger(2)
def add(x, y):
    print("===call add===========")
    time.sleep(2)
    return x + y

print(add(1,2))



#以上代碼執(zhí)行結(jié)果如下:
===call add===========
it's so slow
function add took 2.000923s
3

五、functools.update_wrapper

5.1 概述

語法:

functools.update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS,updated=WRAPPER_UPDATES)

# 
wrapper:包裝函數(shù)、被更新者

wrapped:被包裝函數(shù)、數(shù)據(jù)源

assigned:是元組,用于指定將原始函數(shù)的哪些屬性直接分配給包裝函數(shù)上的匹配屬性。
默認(rèn)是模塊級常量WRAPPER_ASSIGNMENTS,其中是要被覆蓋的屬性'__module__', '__name__', '__qualname__', '__doc__', '__annotations__',即模塊名、名稱、限定名、文檔、參數(shù)注解

updated:使用原函數(shù)的相應(yīng)屬性更新包裝函數(shù)的哪些屬性。
默認(rèn)是模塊級常量WRAPPER_UPDATES,其中是要被更新的屬性(更新wrapper的__dict__,即實例字典)

wrapper增加了一個__wrapped__屬性,保留著wrapped函數(shù)

功能:類似copy_properties功能,用于保護更新包裝函數(shù),使其看起來像被包裝函數(shù)(屬性和被包裝函數(shù)保持一致)

5.2 實例

import datetime, time, functools

def logger(duration, func=lambda name, duration: print('{} took {}s'.format(name, duration))):
    def _logger(fn):
        @functools.wraps(fn)
        def wrapper(*args,**kwargs):
            start = datetime.datetime.now()
            ret = fn(*args,**kwargs)
            delta = (datetime.datetime.now() - start).total_seconds()
            if delta > duration:
                func(fn.__name__, duration)
            return ret
        return wrapper
    return _logger

@logger(5) # add = logger(5)(add)
def add(x,y):
    time.sleep(1)
    return x + y

print(add(5, 6), add.__name__, add.__wrapped__, add.__dict__, sep='\n')




#以上代碼執(zhí)行結(jié)果如下:
11 # 打印的是add(5, 6)結(jié)果
add # add.__name__ 被裝飾后的函數(shù)名
<function add at 0x0000000002A0F378> # add.__wrapped__ 保留著被裝飾函數(shù)
{'__wrapped__': <function add at 0x0000000002A0F378>} # add.__dict__ 屬性字典

5.3 裝飾器的調(diào)用過程

import datetime, time, functools

def logger(fn):
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs)
        delta = (datetime.datetime.now() - start).total_seconds()
        if delta > 3:
            print('so slow')
        return ret

    return wrapper


@logger
def add(x,y):
    pass

@logger
def sub(x,y):
    pass

print(add.__name__, sub.__name__)

查看上面的代碼,思考:

  • logger什么時候執(zhí)行
    • 解釋器讀到 16、20行就被調(diào)用
  • logger執(zhí)行幾次
    • 2次,16、20行
  • wraps裝飾器執(zhí)行幾次
    • 2次,因為 wraps 裝飾器在 logger 才會被調(diào)用,logger執(zhí)行2次,wraps 裝飾器執(zhí)行兩次
  • wrapper的 __name__ 被覆蓋過幾次
    • 各1次
  • print(add.__name__, sub.__name__) 打印了什么
    • add sub

由上可知:

  • 裝飾器函數(shù),在語法糖一被讀取時,就調(diào)用裝飾器函數(shù)了,而不是等到被裝飾函數(shù)被調(diào)用的時候

六、裝飾器的用途和應(yīng)用場景

用途:

裝飾器是AOP面向切面編程 Aspect Oriented Programming 的思想的體現(xiàn)。

    面向?qū)ο笸枰ㄟ^繼承或者組合依賴等方式調(diào)用一些功能,這些功能的代碼往往可能再多個類中出現(xiàn),例如logger功能代碼。這樣造成代碼的重復(fù),增加了耦合。loggger的改變影響所有其它的類或方法。

    而AOP再許喲啊的類或者方法上切下,前后的切入點可以加入增強的功能。讓調(diào)用者和被調(diào)用者解耦,這是一種不修改原來的業(yè)務(wù)代碼,給程序員動態(tài)添加功能的技術(shù)。例如logger函數(shù)就是對業(yè)務(wù)函數(shù)增加日志的功能,而業(yè)務(wù)函數(shù)中應(yīng)該把業(yè)務(wù)無關(guān)的日志功能剝離干凈。

使用場景:

日志,監(jiān)控,權(quán)限,審計,參數(shù)檢查,路由等處理。

這些功能與業(yè)務(wù)功能無關(guān),是很多都需要的公有的功能,所有適合獨立出來,需要的時候,對目標(biāo)對象進行增強。

簡單講:缺什么,補什么。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容