Python函數篇:裝飾器

裝飾器本質上是一個函數,該函數用來處理其他函數,它可以讓其他函數在不需要修改代碼的前提下增加額外的功能,裝飾器的返回值也是一個函數對象。它經常用於有切面需求的場景,比如:插入日誌、性能測試、事務處理、緩存、權限校驗等應用場景。裝飾器是解決這類問題的絕佳設計,有了裝飾器,我們就可以抽離出大量與函數功能本身無關的雷同代碼並繼續重用。概括的講,裝飾器的作用就是爲已經存在的對象添加額外的功能。

嚴格來說,裝飾器只是語法糖,裝飾器是可調用的對象,可以像常規的可調用對象那樣調用,特殊的地方是裝飾器的參數是一個函數

現在有一個新的需求,希望可以記錄下函數的執行時間,於是在代碼中添加日誌代碼:

複製代碼
import time
#遵守開放封閉原則
def foo():
    start = time.time()
    # print(start)  # 1504698634.0291758從1970年1月1號到現在的秒數,那年Unix誕生
    time.sleep(3)
    end = time.time()
    print('spend %s'%(end - start))
foo()
複製代碼

bar()、bar2()也有類似的需求,怎麼做?再在bar函數裏調用時間函數?這樣就造成大量雷同的代碼,爲了減少重複寫代碼,我們可以這樣做,重新定義一個函數:專門設定時間:

複製代碼
import time
def show_time(func):
    start_time=time.time()
    func()
    end_time=time.time()
    print('spend %s'%(end_time-start_time))
 
 
def foo():
    print('hello foo')
    time.sleep(3)
 
show_time(foo)
複製代碼

但是這樣的話,你基礎平臺的函數修改了名字,容易被業務線的人投訴的,因爲我們每次都要將一個函數作爲參數傳遞給show_time函數。而且這種方式已經破壞了原有的代碼邏輯結構,之前執行業務邏輯時,執行運行foo(),但是現在不得不改成show_time(foo)。那麼有沒有更好的方式的呢?當然有,答案就是裝飾器。

複製代碼
def show_time(f):
    def inner():
        start = time.time()
        f()
        end = time.time()
        print('spend %s'%(end - start))
    return inner

@show_time #foo=show_time(f)
def foo():
    print('foo...')
    time.sleep(1)
foo()

def bar():
    print('bar...')
    time.sleep(2)
bar()
複製代碼

輸出結果:

foo...
spend 1.0005607604980469
bar...

函數show_time就是裝飾器,它把真正的業務方法f包裹在函數裏面,看起來像foo被上下時間函數裝飾了。在這個例子中,函數進入和退出時 ,被稱爲一個橫切面(Aspect),這種編程方式被稱爲面向切面的編程(Aspect-Oriented Programming)。

@符號是裝飾器的語法糖,在定義函數的時候使用,避免再一次賦值操作

裝飾器在Python使用如此方便都要歸因於Python的函數能像普通的對象一樣能作爲參數傳遞給其他函數,可以被賦值給其他變量,可以作爲返回值,可以被定義在另外一個函數內。

裝飾器有2個特性,一是可以把被裝飾的函數替換成其他函數, 二是可以在加載模塊時候立即執行

複製代碼
def decorate(func):
    print('running decorate', func)
    def decorate_inner():
        print('running decorate_inner function')
        return func()
    return decorate_inner

@decorate
def func_1():
    print('running func_1')

if __name__ == '__main__':
    print(func_1)
    #running decorate <function func_1 at 0x000001904743DEA0>
    # <function decorate.<locals>.decorate_inner at 0x000001904743DF28>
    func_1()
    #running decorate_inner function
    # running func_1
複製代碼

通過args 和 *kwargs 傳遞被修飾函數中的參數

 

複製代碼
def decorate(func):
    def decorate_inner(*args, **kwargs):
        print(type(args), type(kwargs))
        print('args', args, 'kwargs', kwargs)
        return func(*args, **kwargs)
    return decorate_inner

@decorate
def func_1(*args, **kwargs):
    print(args, kwargs)

if __name__ == '__main__':
    func_1('1', '2', '3', para_1='1', para_2='2', para_3='3')

#返回結果
#<class 'tuple'> <class 'dict'>
# args ('1', '2', '3') kwargs {'para_1': '1', 'para_2': '2', 'para_3': '3'}
# ('1', '2', '3') {'para_1': '1', 'para_2': '2', 'para_3': '3'}
複製代碼

 

 

 

帶參數的被裝飾函數 

複製代碼
import time
# 定長
def show_time(f):
    def inner(x,y):
        start = time.time()
        f(x,y)
        end = time.time()
        print('spend %s'%(end - start))
    return inner

@show_time
def add(a,b):
    print(a+b)
    time.sleep(1)
    
add(1,2)
複製代碼

不定長

複製代碼
import time
#不定長
def show_time(f):
    def inner(*x,**y):
        start = time.time()
        f(*x,**y)
        end = time.time()
        print('spend %s'%(end - start))
    return inner

@show_time
def add(*a,**b):
    sum=0
    for i in a:
        sum+=i
    print(sum)
    time.sleep(1)

add(1,2,3,4)
複製代碼

帶參數的裝飾器

在上面的裝飾器調用中,比如@show_time,該裝飾器唯一的參數就是執行業務的函數。裝飾器的語法允許我們在調用時,提供其它參數,比如@decorator(a)。這樣,就爲裝飾器的編寫和使用提供了更大的靈活性。

複製代碼
import time
def time_logger(flag=0):
    def show_time(func):
        def wrapper(*args, **kwargs):
            start_time = time.time()
            func(*args, **kwargs)
            end_time = time.time()
            print('spend %s' % (end_time - start_time))
            if flag:
                print('將這個操作的時間記錄到日誌中')
        return wrapper
    return show_time

@time_logger(flag=1)
def add(*args, **kwargs):
    time.sleep(1)
    sum = 0
    for i in args:
        sum += i
    print(sum)
add(1, 2, 5)
複製代碼

@time_logger(flag=1) 做了兩件事:

    (1)time_logger(1):得到閉包函數show_time,裏面保存環境變量flag

    (2)@show_time   :add=show_time(add)

上面的time_logger是允許帶參數的裝飾器。它實際上是對原有裝飾器的一個函數封裝,並返回一個裝飾器(一個含有參數的閉包函數)。當我 們使用@time_logger(1)調用的時候,Python能夠發現這一層的封裝,並把參數傳遞到裝飾器的環境中。

疊放裝飾器

執行順序是什麼

如果一個函數被多個裝飾器修飾,其實應該是該函數先被最裏面的裝飾器修飾後(下面例子中函數main()先被inner裝飾,變成新的函數),變成另一個函數後,再次被裝飾器修飾

複製代碼
def outer(func):
    print('enter outer', func)
    def wrapper():
        print('running outer')
        func()
    return wrapper

def inner(func):
    print('enter inner', func)
    def wrapper():
        print('running inner')
        func()
    return wrapper

@outer
@inner
def main():
    print('running main')

if __name__ == '__main__':
    main()

#返回結果
# enter inner <function main at 0x000001A9F2BCDF28>
# enter outer <function inner.<locals>.wrapper at 0x000001A9F2BD5048>
# running outer
# running inner
# running main
複製代碼

類裝飾器

相比函數裝飾器,類裝飾器具有靈活度大、高內聚、封裝性等優點。使用類裝飾器還可以依靠類內部的__call__方法,當使用 @ 形式將裝飾器附加到函數上時,就會調用此方法。

複製代碼
import time

class Foo(object):
    def __init__(self, func):
        self._func = func

    def __call__(self):
        start_time=time.time()
        self._func()
        end_time=time.time()
        print('spend %s'%(end_time-start_time))

@Foo  #bar=Foo(bar)
def bar():
    print ('bar')
    time.sleep(2)
    
bar()    #bar=Foo(bar)()>>>>>>>沒有嵌套關係了,直接active Foo的 __call__方法
複製代碼

標準庫中有多種裝飾器

例如:裝飾方法的函數有property, classmethod, staticmethod; functools模塊中的lru_cache, singledispatch,  wraps 等等

from functools import lru_cache

from functools import singledispatch

from functools import wraps

 

functools.wraps使用裝飾器極大地複用了代碼,但是他有一個缺點就是原函數的元信息不見了,比如函數的docstring、__name__、參數列表,先看例子:

複製代碼
def foo():
    print("hello foo")
print(foo.__name__)# foo

def logged(func):
    def wrapper(*args, **kwargs):
        print (func.__name__ + " was called")
        return func(*args, **kwargs)
    return wrapper

@logged
def cal(x):
    resul=x + x * x
    print(resul)

cal(2)
#6
#cal was called
print(cal.__name__)# wrapper
print(cal.__doc__)#None
#函數f被wrapper取代了,當然它的docstring,__name__就是變成了wrapper函數的信息了。
複製代碼
好在我們有functools.wraps,wraps本身也是一個裝飾器,它能把原函數的元信息拷貝到裝飾器函數中,這使得裝飾器函數也有和原函數一樣的元信息了。
複製代碼
from functools import wraps

def logged(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(func.__name__ + " was called")
        return func(*args, **kwargs)
    return wrapper

@logged
def cal(x):
    return x + x * x

print(cal.__name__)  # cal
複製代碼

使用裝飾器會產生我們可能不希望出現的副作用, 例如:改變被修飾函數名稱,對於調試器或者對象序列化器等需要使用內省機制的那些工具,可能會無法正常運行;

其實調用裝飾器後,會將同一個作用域中原來函數同名的那個變量(例如下面的func_1),重新賦值爲裝飾器返回的對象;使用@wraps後,會把與內部函數(被修飾函數,例如下面的func_1)相關的重要元數據全部複製到外圍函數(例如下面的decorate_inner)

複製代碼
from functools import wraps

def decorate(func):
    print('running decorate', func)
    @wraps(func)
    def decorate_inner():
        print('running decorate_inner function', decorate_inner)
        return func()
    return decorate_inner

@decorate
def func_1():
    print('running func_1', func_1)

if __name__ == '__main__':
    func_1()

#輸出結果
#running decorate <function func_1 at 0x0000023E8DBD78C8>
# running decorate_inner function <function func_1 at 0x0000023E8DBD7950>
# running func_1 <function func_1 at 0x0000023E8DBD7950>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章