1. 程式人生 > 其它 >Python的裝飾器的理解***** ython修飾器(裝飾器)以及wraps

Python的裝飾器的理解***** ython修飾器(裝飾器)以及wraps

ython修飾器(裝飾器)以及wraps

Python裝飾器(decorator)是在程式開發中經常使用到的功能,合理使用裝飾器,能讓我們的程式如虎添翼。

裝飾器的引入

初期及問題的誕生

假如現在在一個公司,有A B C三個業務部門,還有S一個基礎服務部門,目前呢,S部門提供了兩個函式,供其他部門呼叫,函式如下:

def f1():
    print('f1 called')

def f2():
    print('f2 called')

在初期,其他部門這樣呼叫是沒有問題的,隨著公司業務的發展,現在S部門需要對函式呼叫假如許可權驗證,如果有許可權的話,才能進行呼叫,否則呼叫失敗。考慮一下,如果是我們,該怎麼做呢?

方案集合
1、讓呼叫方也就是ABC部門在呼叫的時候,先主動進行許可權驗證
2、S部門在對外提供的函式中,首先進行許可權認證,然後再進行真正的函式操作
問題
    方案一,將本不該暴露給外層的許可權認證,暴露在使用方面前,同時如果有多個部門呢,要每個部門每個人都要周知到,你還不缺定別人一定會這麼做,不靠譜。。。
    方案二,看似看行,可是當S部門對外提供更多的需要進行許可權驗證方法時,每個函式都要呼叫許可權驗證,同樣也實在費勁,不利於程式碼的維護性和擴充套件性

那麼,有沒有一種方法能夠遵循程式碼的開放閉合原則,來完美的解決此問題呢?

裝飾器引入
答案肯定是有的,不然真的是弱爆了。先看程式碼

def w1(func):
    def inner():
        print('...驗證許可權...')
        func()
    return inner
@w1
def f1():
    print('f1 called')

@w1
def f2():
    print('f2 called')
f1()
f2()

輸出結果為

...驗證許可權...
f1 called
...驗證許可權...
f2 called

可以通過程式碼及輸出看到,在呼叫f1 f2 函式時,成功進行了許可權驗證,那麼是怎麼做到的呢?其實這裡就使用到了裝飾器,通過定義一個閉包函式w1,在我們呼叫函式上通過關鍵詞@w1,這樣就對f1 f2函式完成了裝飾。

裝飾器原理
首先,開看我們的裝飾器函式w1,該函式接收一個引數func,其實就是接收一個方法名,w1內部又定義一個函式inner,在inner函式中增加許可權校驗,並在驗證完許可權後呼叫傳進來的引數func,同時w1的返回值為內部函式inner,其實就是一個閉包函式。

然後,再來看一下,在f1上增加@w1,那這是什麼意思呢?當python直譯器執行到這句話的時候,會去呼叫w1函式,同時將被裝飾的函式名作為引數傳入(此時為f1),根據閉包一文分析,在執行w1函式的時候,此時直接把inner函式返回了,同時把它賦值給f1,此時的f1已經不是未加裝飾時的f1了,而是指向了w1.inner函式地址。相當於f1=w1(f1)

接下來,在呼叫f1()的時候,其實呼叫的是w1.inner函式,那麼此時就會先執行許可權驗證,然後再呼叫原來的f1(),該處的f1就是通過裝飾傳進來的引數f1。

這樣下來,就完成了對f1的裝飾,實現了許可權驗證。

裝飾器知識點

執行時機

瞭解了裝飾器的原理後,那麼它的執行時機是什麼樣呢,接下來就來看一下。 
國際慣例,先上程式碼

def w1(fun):
    print('...裝飾器開始裝飾...')
    def inner():
        print('...驗證許可權...')
        fun()
    return inner
@w1
def test():
    print('test')
test()

輸出結果為

...裝飾器開始裝飾...
...驗證許可權...
test

由此可以發現,當python直譯器執行到@w1時,就開始進行裝飾了,相當於執行了如下程式碼:

test = w1(test)

兩個裝飾器執行流程和裝飾結果

當有兩個或兩個以上裝飾器裝飾一個函式時,那麼執行流程和裝飾結果是什麼樣的呢?同樣,還是以程式碼來說明問題。

def makeBold(fun):
    print('----a----')

    def inner():
        print('----1----')
        return '<b>' + fun() + '</b>'
    return inner
def makeItalic(fun):
    print('----b----')

    def inner():
        print('----2----')
        return '<i>' + fun() + '</i>'
    return inner
@makeBold
@makeItalic
def test():
    print('----c----')
    print('----3----')
    return 'hello python decorator'
ret = test()
print(ret)

輸出結果:

----b----
----a----
----1----
----2----
----c----
----3----
<b><i>hello python decorator</i></b>

可以發現,先用第二個裝飾器(makeItalic)進行裝飾,接著再用第一個裝飾器(makeBold)進行裝飾,而在呼叫過程中,先執行第一個裝飾器(makeBold),接著再執行第二個裝飾器(makeItalic)。

為什麼呢,分兩步來分析一下。

1、裝飾時機 通過上面裝飾時機的介紹,我們可以知道,在執行到@makeBold的時候,需要對下面的函式進行裝飾,此時直譯器繼續往下走,發現並不是一個函式名,而又是一個裝飾器,這時候,@makeBold裝飾器暫停執行,而接著執行接下來的裝飾器@makeItalic,接著把test函式名傳入裝飾器函式,從而列印’b’,在makeItalic裝飾完後,此時的test指向makeItalic的inner函式地址,這時候有返回來執行@makeBold,接著把新test傳入makeBold裝飾器函式中,因此列印了’a’。
2、在呼叫test函式的時候,根據上述分析,此時test指向makeBold.inner函式,因此會先列印‘1‘,接下來,在呼叫fun()的時候,其實是呼叫的makeItalic.inner()函式,所以列印‘2‘,在makeItalic.inner中,呼叫的fun其實才是我們最原聲的test函式,所以列印原test函式中的‘c‘,‘3‘,所以在一層層調完之後,列印的結果為<b><i>hello python decorator</i></b> 。

對無參函式進行裝飾

上面例子中的f1 f2都是對無參函式的裝飾,不再單獨舉例

對有引數函式進行裝飾

在使用中,有的函式可能會帶有引數,那麼這種如何處理呢? 
程式碼優先:

def w_say(fun):
    """
    如果原函式有引數,那閉包函式必須保持引數個數一致,並且將引數傳遞給原方法
    """

    def inner(name):
        """
        如果被裝飾的函式有行參,那麼閉包函式必須有引數
        :param name:
        :return:
        """
        print('say inner called')
        fun(name)

    return inner


@w_say
def hello(name):
    print('hello ' + name)


hello('wangcai')

輸出結果為:

say inner called
hello wangcai

具體說明程式碼註釋已經有了,就不再單獨說明了。 
此時,也許你就會問了,那是一個引數的,如果多個或者不定長引數呢,該如何處理呢?看看下面的程式碼你就秒懂了。

def w_add(func):
    def inner(*args, **kwargs):
        print('add inner called')
        func(*args, **kwargs)
    return inner
@w_add
def add(a, b):
    print('%d + %d = %d' % (a, b, a + b))
@w_add
def add2(a, b, c):
    print('%d + %d + %d = %d' % (a, b, c, a + b + c))
add(2, 4)
add2(2, 4, 6)

輸出結果為:

add inner called
2 + 4 = 6
add inner called
2 + 4 + 6 = 12

利用python的可變引數輕鬆實現裝飾帶引數的函式。

對帶有返回值的函式進行裝飾

下面對有返回值的函式進行裝飾,按照之前的寫法,程式碼是這樣的

def w_test(func):
    def inner():
        print('w_test inner called start')
        func()
        print('w_test inner called end')
    return inner
@w_test
def test():
    print('this is test fun')
    return 'hello'
ret = test()
print('ret value is %s' % ret)

輸出結果為:

w_test inner called start
this is test fun
w_test inner called end
ret value is None

可以發現,此時,並沒有輸出test函式的‘hello’,而是None,那是為什麼呢,可以發現,在inner函式中對test進行了呼叫,但是沒有接受不了返回值,也沒有進行返回,那麼預設就是None了,知道了原因,那麼來修改一下程式碼:

def w_test(func):
    def inner():
        print('w_test inner called start')
        str = func()
        print('w_test inner called end')
        return str
    return inner
@w_test
def test():
    print('this is test fun')
    return 'hello'
ret = test()
print('ret value is %s' % ret)

輸出結果:

w_test inner called start
this is test fun
w_test inner called end
ret value is hello

這樣就達到預期,完成對帶返回值引數的函式進行裝飾。

帶引數的裝飾器

介紹了對帶引數的函式和有返回值的函式進行裝飾,那麼有沒有帶引數的裝飾器呢,如果有的話,又有什麼用呢? 
答案肯定是有的,接下來通過程式碼來看一下吧。

def func_args(pre='xiaoqiang'):
    def w_test_log(func):
        def inner():
            print('...記錄日誌...visitor is %s' % pre)
            func()
        return inner
    return w_test_log
# 帶有引數的裝飾器能夠起到在執行時,有不同的功能
# 先執行func_args('wangcai'),返回w_test_log函式的引用
# @w_test_log
# 使用@w_test_log對test_log進行裝飾
@func_args('wangcai')
def test_log():
    print('this is test log')
test_log()

輸出結果為:

...記錄日誌...visitor is wangcai
this is test log

簡單理解,帶引數的裝飾器就是在原閉包的基礎上又加了一層閉包,通過外層函式func_args的返回值w_test_log就看出來了,具體執行流程在註釋裡已經說明了。 
好處就是可以在執行時,針對不同的引數做不同的應用功能處理。

通用裝飾器
介紹了這麼多,在實際應用中,如果針對沒個類別的函式都要寫一個裝飾器的話,估計就累死了,那麼有沒有通用萬能裝飾器呢,答案肯定是有的,廢話不多說,直接上程式碼。

def w_test(func):
    def inner(*args, **kwargs):
        ret = func(*args, **kwargs)
        return ret
    return inner
@w_test
def test():
    print('test called')
@w_test
def test1():
    print('test1 called')
    return 'python'
@w_test
def test2(a):
    print('test2 called and value is %d ' % a)
test()
test1()
test2(9)

輸出結果為:

test called
test1 called
test2 called and value is 9 

把上面幾種示例結合起來,就完成了通用裝飾器的功能,原理都同上,就不過多廢話了。

類裝飾器

裝飾器函式其實是一個介面約束,它必須接受一個callable物件作為引數,然後返回一個callable物件。 
在python中,一般callable物件都是函式,但是也有例外。比如只要某個物件重寫了call方法,那麼這個物件就是callable的。

當建立一個物件後,直接去執行這個物件,那麼是會丟擲異常的,因為他不是callable,無法直接執行,但進行修改後,就可以直接執行呼叫了,如下

class Test(object):
    def __call__(self, *args, **kwargs):
        print('call called')


t = Test()
print(t())

輸出為:

call called

下面,引入正題,看一下如何用類裝飾函式。

class Test(object):
    def __init__(self, func):
        print('test init')
        print('func name is %s ' % func.__name__)
        self.__func = func
    def __call__(self, *args, **kwargs):
        print('裝飾器中的功能')
        self.__func()
@Test
def test():
    print('this is test func')
test()

輸出結果為:

test init
func name is test 
裝飾器中的功能
this is test func

和之前的原理一樣,當python直譯器執行到到@Test時,會把當前test函式作為引數傳入Test物件,呼叫init方法,同時將test函式指向建立的Test物件,那麼在接下來執行test()的時候,其實就是直接對建立的物件進行呼叫,執行其call方法。

預備知識

在瞭解wraps修飾器之前,我們首先要了解partialupdate_wrapper這兩個函式,因為在wraps的程式碼中,用到了這兩個函式。

partial

首先說partial函式,在官方文件的描述中,這個函式的宣告如下:functools.partial(func, *args, **keywords)。它的作用就是返回一個partial物件,當這個partial物件被呼叫的時候,就像通過func(*args, **kwargs)的形式來呼叫func函式一樣。如果有額外的 位置引數(args) 或者 關鍵字引數(*kwargs) 被傳給了這個partial物件,那它們也都會被傳遞給func函式,如果一個引數被多次傳入,那麼後面的值會覆蓋前面的值。

個人感覺這個函式很像C++中的bind函式,都是把某個函式的某個引數固定,從而構造出一個新的函式來。比如下面這個例子:

from functools import partial

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

# 這裡創造了一個新的函式add2,只接受一個整型引數,然後將這個引數統一加上2
add2 = partial(add, y=2)

add2(3)  # 這裡將會輸出5

這個函式是使用C而不是Python實現的,但是官方文件中給出了Python實現的程式碼,如下所示,大家可以進行參考:

def partial(func, *args, **keywords):
    def newfunc(*fargs, **fkeywords):
        newkeywords = keywords.copy()
        newkeywords.update(fkeywords)
        return func(*args, *fargs, **newkeywords)
    newfunc.func = func
    newfunc.args = args
    newfunc.keywords = keywords
    return newfunc

update_wrapper

接下來,我們再來聊一聊update_wrapper這個函式,顧名思義,這個函式就是用來更新修飾器函式的,具體更新些什麼呢,我們可以直接把它的原始碼搬過來看一下:

WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
                       '__annotations__')
WRAPPER_UPDATES = ('__dict__',)
def update_wrapper(wrapper,
                   wrapped,
                   assigned = WRAPPER_ASSIGNMENTS,
                   updated = WRAPPER_UPDATES):
    for attr in assigned:
        try:
            value = getattr(wrapped, attr)
        except AttributeError:
            pass
        else:
            setattr(wrapper, attr, value)
    for attr in updated:
        getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
    wrapper.__wrapped__ = wrapped
    return wrapper

大家可以發現,這個函式的作用就是從 被修飾的函式(wrapped) 中取出一些屬性值來,賦值給 修飾器函式(wrapper) 。為什麼要這麼做呢,我們看下面這個例子。

自定義修飾器v1

首先我們寫個自定義的修飾器,沒有任何的功能,僅有文件字串,如下所示:

def wrapper(f):
    def wrapper_function(*args, **kwargs):
        """這個是修飾函式"""
        return f(*args, **kwargs)
    return wrapper_function
    
@wrapper
def wrapped():
    """這個是被修飾的函式"""
    print('wrapped')

print(wrapped.__doc__)  # 輸出`這個是修飾函式`
print(wrapped.__name__)  # 輸出`wrapper_function`

從上面的例子我們可以看到,我想要獲取wrapped這個被修飾函式的文件字串,但是卻獲取成了wrapper_function的文件字串,wrapped函式的名字也變成了wrapper_function函式的名字。這是因為給wrapped新增上@wrapper修飾器相當於執行了一句wrapped = wrapper(wrapped),執行完這條語句之後,wrapped函式就變成了wrapper_function函式。遇到這種情況該怎麼辦呢,首先我們可以手動地在wrapper函式中更改wrapper_function__doc____name__屬性,但聰明的你肯定也想到了,我們可以直接用update_wrapper函式來實現這個功能。

自定義修飾器v2

我們對上面定義的修飾器稍作修改,添加了一句update_wrapper(wrapper_function, f)

from functools import update_wrapper

def wrapper(f):
    def wrapper_function(*args, **kwargs):
        """這個是修飾函式"""
        return f(*args, **kwargs)
    update_wrapper(wrapper_function, f)  # <<  添加了這條語句
    return wrapper_function
    
@wrapper
def wrapped():
    """這個是被修飾的函式"""
    print('wrapped')


print(wrapped.__doc__)  # 輸出`這個是被修飾的函式`
print(wrapped.__name__)  # 輸出`wrapped`

此時我們可以發現,__doc____name__屬性已經能夠按我們預想的那樣顯示了,除此之外,update_wrapper函式也對__module____dict__等屬性進行了更改和更新。

wraps修飾器

OK,至此,我們已經瞭解了partialupdate_wrapper這兩個函式的功能,接下來我們翻出wraps修飾器的原始碼:

WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
                       '__annotations__')
WRAPPER_UPDATES = ('__dict__',)
def wraps(wrapped,
          assigned = WRAPPER_ASSIGNMENTS,
          updated = WRAPPER_UPDATES):
    return partial(update_wrapper, wrapped=wrapped,
                   assigned=assigned, updated=updated)

沒錯,就是這麼的簡單,只有這麼一句,我們可以看出,wraps函式其實就是一個修飾器版的update_wrapper函式,它的功能和update_wrapper是一模一樣的。我們可以修改我們上面的自定義修飾器的例子,做出一個更方便閱讀的版本。

自定義修飾器v3

from functools import wraps

def wrapper(f):
    @wraps(f)
    def wrapper_function(*args, **kwargs):
        """這個是修飾函式"""
        return f(*args, **kwargs)
    return wrapper_function
    
@wrapper
def wrapped():
    """這個是被修飾的函式
    """
    print('wrapped')

print(wrapped.__doc__)  # 輸出`這個是被修飾的函式`
print(wrapped.__name__)  # 輸出`wrapped`

至此,我想大家應該明白wraps這個修飾器的作用了吧,就是將 被修飾的函式(wrapped) 的一些屬性值賦值給 修飾器函式(wrapper) ,最終讓屬性的顯示更符合我們的直覺。

參考連結

原博文連結:https://www.cnblogs.com/slysky/p/9777424.html

https://segmentfault.com/a/1190000009398663

https://blog.csdn.net/u010358168/article/details/77773199

Python裝飾器(decorator)是在程式開發中經常使用到的功能,合理使用裝飾器,能讓我們的程式如虎添翼。

裝飾器的引入

初期及問題的誕生

假如現在在一個公司,有A B C三個業務部門,還有S一個基礎服務部門,目前呢,S部門提供了兩個函式,供其他部門呼叫,函式如下:

def f1():
    print('f1 called')

def f2():
    print('f2 called')

在初期,其他部門這樣呼叫是沒有問題的,隨著公司業務的發展,現在S部門需要對函式呼叫假如許可權驗證,如果有許可權的話,才能進行呼叫,否則呼叫失敗。考慮一下,如果是我們,該怎麼做呢?

方案集合
1、讓呼叫方也就是ABC部門在呼叫的時候,先主動進行許可權驗證
2、S部門在對外提供的函式中,首先進行許可權認證,然後再進行真正的函式操作
問題
    方案一,將本不該暴露給外層的許可權認證,暴露在使用方面前,同時如果有多個部門呢,要每個部門每個人都要周知到,你還不缺定別人一定會這麼做,不靠譜。。。
    方案二,看似看行,可是當S部門對外提供更多的需要進行許可權驗證方法時,每個函式都要呼叫許可權驗證,同樣也實在費勁,不利於程式碼的維護性和擴充套件性

那麼,有沒有一種方法能夠遵循程式碼的開放閉合原則,來完美的解決此問題呢?

裝飾器引入
答案肯定是有的,不然真的是弱爆了。先看程式碼

def w1(func):
    def inner():
        print('...驗證許可權...')
        func()
    return inner
@w1
def f1():
    print('f1 called')

@w1
def f2():
    print('f2 called')
f1()
f2()

輸出結果為

...驗證許可權...
f1 called
...驗證許可權...
f2 called

可以通過程式碼及輸出看到,在呼叫f1 f2 函式時,成功進行了許可權驗證,那麼是怎麼做到的呢?其實這裡就使用到了裝飾器,通過定義一個閉包函式w1,在我們呼叫函式上通過關鍵詞@w1,這樣就對f1 f2函式完成了裝飾。

裝飾器原理
首先,開看我們的裝飾器函式w1,該函式接收一個引數func,其實就是接收一個方法名,w1內部又定義一個函式inner,在inner函式中增加許可權校驗,並在驗證完許可權後呼叫傳進來的引數func,同時w1的返回值為內部函式inner,其實就是一個閉包函式。

然後,再來看一下,在f1上增加@w1,那這是什麼意思呢?當python直譯器執行到這句話的時候,會去呼叫w1函式,同時將被裝飾的函式名作為引數傳入(此時為f1),根據閉包一文分析,在執行w1函式的時候,此時直接把inner函式返回了,同時把它賦值給f1,此時的f1已經不是未加裝飾時的f1了,而是指向了w1.inner函式地址。相當於f1=w1(f1)

接下來,在呼叫f1()的時候,其實呼叫的是w1.inner函式,那麼此時就會先執行許可權驗證,然後再呼叫原來的f1(),該處的f1就是通過裝飾傳進來的引數f1。

這樣下來,就完成了對f1的裝飾,實現了許可權驗證。

裝飾器知識點

執行時機

瞭解了裝飾器的原理後,那麼它的執行時機是什麼樣呢,接下來就來看一下。 
國際慣例,先上程式碼

def w1(fun):
    print('...裝飾器開始裝飾...')
    def inner():
        print('...驗證許可權...')
        fun()
    return inner
@w1
def test():
    print('test')
test()

輸出結果為

...裝飾器開始裝飾...
...驗證許可權...
test

由此可以發現,當python直譯器執行到@w1時,就開始進行裝飾了,相當於執行了如下程式碼:

test = w1(test)

兩個裝飾器執行流程和裝飾結果

當有兩個或兩個以上裝飾器裝飾一個函式時,那麼執行流程和裝飾結果是什麼樣的呢?同樣,還是以程式碼來說明問題。

def makeBold(fun):
    print('----a----')

    def inner():
        print('----1----')
        return '<b>' + fun() + '</b>'
    return inner
def makeItalic(fun):
    print('----b----')

    def inner():
        print('----2----')
        return '<i>' + fun() + '</i>'
    return inner
@makeBold
@makeItalic
def test():
    print('----c----')
    print('----3----')
    return 'hello python decorator'
ret = test()
print(ret)

輸出結果:

----b----
----a----
----1----
----2----
----c----
----3----
<b><i>hello python decorator</i></b>

可以發現,先用第二個裝飾器(makeItalic)進行裝飾,接著再用第一個裝飾器(makeBold)進行裝飾,而在呼叫過程中,先執行第一個裝飾器(makeBold),接著再執行第二個裝飾器(makeItalic)。

為什麼呢,分兩步來分析一下。

1、裝飾時機 通過上面裝飾時機的介紹,我們可以知道,在執行到@makeBold的時候,需要對下面的函式進行裝飾,此時直譯器繼續往下走,發現並不是一個函式名,而又是一個裝飾器,這時候,@makeBold裝飾器暫停執行,而接著執行接下來的裝飾器@makeItalic,接著把test函式名傳入裝飾器函式,從而列印’b’,在makeItalic裝飾完後,此時的test指向makeItalic的inner函式地址,這時候有返回來執行@makeBold,接著把新test傳入makeBold裝飾器函式中,因此列印了’a’。
2、在呼叫test函式的時候,根據上述分析,此時test指向makeBold.inner函式,因此會先列印‘1‘,接下來,在呼叫fun()的時候,其實是呼叫的makeItalic.inner()函式,所以列印‘2‘,在makeItalic.inner中,呼叫的fun其實才是我們最原聲的test函式,所以列印原test函式中的‘c‘,‘3‘,所以在一層層調完之後,列印的結果為<b><i>hello python decorator</i></b> 。

對無參函式進行裝飾

上面例子中的f1 f2都是對無參函式的裝飾,不再單獨舉例

對有引數函式進行裝飾

在使用中,有的函式可能會帶有引數,那麼這種如何處理呢? 
程式碼優先:

def w_say(fun):
    """
    如果原函式有引數,那閉包函式必須保持引數個數一致,並且將引數傳遞給原方法
    """

    def inner(name):
        """
        如果被裝飾的函式有行參,那麼閉包函式必須有引數
        :param name:
        :return:
        """
        print('say inner called')
        fun(name)

    return inner


@w_say
def hello(name):
    print('hello ' + name)


hello('wangcai')

輸出結果為:

say inner called
hello wangcai

具體說明程式碼註釋已經有了,就不再單獨說明了。 
此時,也許你就會問了,那是一個引數的,如果多個或者不定長引數呢,該如何處理呢?看看下面的程式碼你就秒懂了。

def w_add(func):
    def inner(*args, **kwargs):
        print('add inner called')
        func(*args, **kwargs)
    return inner
@w_add
def add(a, b):
    print('%d + %d = %d' % (a, b, a + b))
@w_add
def add2(a, b, c):
    print('%d + %d + %d = %d' % (a, b, c, a + b + c))
add(2, 4)
add2(2, 4, 6)

輸出結果為:

add inner called
2 + 4 = 6
add inner called
2 + 4 + 6 = 12

利用python的可變引數輕鬆實現裝飾帶引數的函式。

對帶有返回值的函式進行裝飾

下面對有返回值的函式進行裝飾,按照之前的寫法,程式碼是這樣的

def w_test(func):
    def inner():
        print('w_test inner called start')
        func()
        print('w_test inner called end')
    return inner
@w_test
def test():
    print('this is test fun')
    return 'hello'
ret = test()
print('ret value is %s' % ret)

輸出結果為:

w_test inner called start
this is test fun
w_test inner called end
ret value is None

可以發現,此時,並沒有輸出test函式的‘hello’,而是None,那是為什麼呢,可以發現,在inner函式中對test進行了呼叫,但是沒有接受不了返回值,也沒有進行返回,那麼預設就是None了,知道了原因,那麼來修改一下程式碼:

def w_test(func):
    def inner():
        print('w_test inner called start')
        str = func()
        print('w_test inner called end')
        return str
    return inner
@w_test
def test():
    print('this is test fun')
    return 'hello'
ret = test()
print('ret value is %s' % ret)

輸出結果:

w_test inner called start
this is test fun
w_test inner called end
ret value is hello

這樣就達到預期,完成對帶返回值引數的函式進行裝飾。

帶引數的裝飾器

介紹了對帶引數的函式和有返回值的函式進行裝飾,那麼有沒有帶引數的裝飾器呢,如果有的話,又有什麼用呢? 
答案肯定是有的,接下來通過程式碼來看一下吧。

def func_args(pre='xiaoqiang'):
    def w_test_log(func):
        def inner():
            print('...記錄日誌...visitor is %s' % pre)
            func()
        return inner
    return w_test_log
# 帶有引數的裝飾器能夠起到在執行時,有不同的功能
# 先執行func_args('wangcai'),返回w_test_log函式的引用
# @w_test_log
# 使用@w_test_log對test_log進行裝飾
@func_args('wangcai')
def test_log():
    print('this is test log')
test_log()

輸出結果為:

...記錄日誌...visitor is wangcai
this is test log

簡單理解,帶引數的裝飾器就是在原閉包的基礎上又加了一層閉包,通過外層函式func_args的返回值w_test_log就看出來了,具體執行流程在註釋裡已經說明了。 
好處就是可以在執行時,針對不同的引數做不同的應用功能處理。

通用裝飾器
介紹了這麼多,在實際應用中,如果針對沒個類別的函式都要寫一個裝飾器的話,估計就累死了,那麼有沒有通用萬能裝飾器呢,答案肯定是有的,廢話不多說,直接上程式碼。

def w_test(func):
    def inner(*args, **kwargs):
        ret = func(*args, **kwargs)
        return ret
    return inner
@w_test
def test():
    print('test called')
@w_test
def test1():
    print('test1 called')
    return 'python'
@w_test
def test2(a):
    print('test2 called and value is %d ' % a)
test()
test1()
test2(9)

輸出結果為:

test called
test1 called
test2 called and value is 9 

把上面幾種示例結合起來,就完成了通用裝飾器的功能,原理都同上,就不過多廢話了。

類裝飾器

裝飾器函式其實是一個介面約束,它必須接受一個callable物件作為引數,然後返回一個callable物件。 
在python中,一般callable物件都是函式,但是也有例外。比如只要某個物件重寫了call方法,那麼這個物件就是callable的。

當建立一個物件後,直接去執行這個物件,那麼是會丟擲異常的,因為他不是callable,無法直接執行,但進行修改後,就可以直接執行呼叫了,如下

class Test(object):
    def __call__(self, *args, **kwargs):
        print('call called')


t = Test()
print(t())

輸出為:

call called

下面,引入正題,看一下如何用類裝飾函式。

class Test(object):
    def __init__(self, func):
        print('test init')
        print('func name is %s ' % func.__name__)
        self.__func = func
    def __call__(self, *args, **kwargs):
        print('裝飾器中的功能')
        self.__func()
@Test
def test():
    print('this is test func')
test()

輸出結果為:

test init
func name is test 
裝飾器中的功能
this is test func

和之前的原理一樣,當python直譯器執行到到@Test時,會把當前test函式作為引數傳入Test物件,呼叫init方法,同時將test函式指向建立的Test物件,那麼在接下來執行test()的時候,其實就是直接對建立的物件進行呼叫,執行其call方法。

預備知識

在瞭解wraps修飾器之前,我們首先要了解partialupdate_wrapper這兩個函式,因為在wraps的程式碼中,用到了這兩個函式。

partial

首先說partial函式,在官方文件的描述中,這個函式的宣告如下:functools.partial(func, *args, **keywords)。它的作用就是返回一個partial物件,當這個partial物件被呼叫的時候,就像通過func(*args, **kwargs)的形式來呼叫func函式一樣。如果有額外的 位置引數(args) 或者 關鍵字引數(*kwargs) 被傳給了這個partial物件,那它們也都會被傳遞給func函式,如果一個引數被多次傳入,那麼後面的值會覆蓋前面的值。

個人感覺這個函式很像C++中的bind函式,都是把某個函式的某個引數固定,從而構造出一個新的函式來。比如下面這個例子:

from functools import partial

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

# 這裡創造了一個新的函式add2,只接受一個整型引數,然後將這個引數統一加上2
add2 = partial(add, y=2)

add2(3)  # 這裡將會輸出5

這個函式是使用C而不是Python實現的,但是官方文件中給出了Python實現的程式碼,如下所示,大家可以進行參考:

def partial(func, *args, **keywords):
    def newfunc(*fargs, **fkeywords):
        newkeywords = keywords.copy()
        newkeywords.update(fkeywords)
        return func(*args, *fargs, **newkeywords)
    newfunc.func = func
    newfunc.args = args
    newfunc.keywords = keywords
    return newfunc

update_wrapper

接下來,我們再來聊一聊update_wrapper這個函式,顧名思義,這個函式就是用來更新修飾器函式的,具體更新些什麼呢,我們可以直接把它的原始碼搬過來看一下:

WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
                       '__annotations__')
WRAPPER_UPDATES = ('__dict__',)
def update_wrapper(wrapper,
                   wrapped,
                   assigned = WRAPPER_ASSIGNMENTS,
                   updated = WRAPPER_UPDATES):
    for attr in assigned:
        try:
            value = getattr(wrapped, attr)
        except AttributeError:
            pass
        else:
            setattr(wrapper, attr, value)
    for attr in updated:
        getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
    wrapper.__wrapped__ = wrapped
    return wrapper

大家可以發現,這個函式的作用就是從 被修飾的函式(wrapped) 中取出一些屬性值來,賦值給 修飾器函式(wrapper) 。為什麼要這麼做呢,我們看下面這個例子。

自定義修飾器v1

首先我們寫個自定義的修飾器,沒有任何的功能,僅有文件字串,如下所示:

def wrapper(f):
    def wrapper_function(*args, **kwargs):
        """這個是修飾函式"""
        return f(*args, **kwargs)
    return wrapper_function
    
@wrapper
def wrapped():
    """這個是被修飾的函式"""
    print('wrapped')

print(wrapped.__doc__)  # 輸出`這個是修飾函式`
print(wrapped.__name__)  # 輸出`wrapper_function`

從上面的例子我們可以看到,我想要獲取wrapped這個被修飾函式的文件字串,但是卻獲取成了wrapper_function的文件字串,wrapped函式的名字也變成了wrapper_function函式的名字。這是因為給wrapped新增上@wrapper修飾器相當於執行了一句wrapped = wrapper(wrapped),執行完這條語句之後,wrapped函式就變成了wrapper_function函式。遇到這種情況該怎麼辦呢,首先我們可以手動地在wrapper函式中更改wrapper_function__doc____name__屬性,但聰明的你肯定也想到了,我們可以直接用update_wrapper函式來實現這個功能。

自定義修飾器v2

我們對上面定義的修飾器稍作修改,添加了一句update_wrapper(wrapper_function, f)

from functools import update_wrapper

def wrapper(f):
    def wrapper_function(*args, **kwargs):
        """這個是修飾函式"""
        return f(*args, **kwargs)
    update_wrapper(wrapper_function, f)  # <<  添加了這條語句
    return wrapper_function
    
@wrapper
def wrapped():
    """這個是被修飾的函式"""
    print('wrapped')


print(wrapped.__doc__)  # 輸出`這個是被修飾的函式`
print(wrapped.__name__)  # 輸出`wrapped`

此時我們可以發現,__doc____name__屬性已經能夠按我們預想的那樣顯示了,除此之外,update_wrapper函式也對__module____dict__等屬性進行了更改和更新。

wraps修飾器

OK,至此,我們已經瞭解了partialupdate_wrapper這兩個函式的功能,接下來我們翻出wraps修飾器的原始碼:

WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
                       '__annotations__')
WRAPPER_UPDATES = ('__dict__',)
def wraps(wrapped,
          assigned = WRAPPER_ASSIGNMENTS,
          updated = WRAPPER_UPDATES):
    return partial(update_wrapper, wrapped=wrapped,
                   assigned=assigned, updated=updated)

沒錯,就是這麼的簡單,只有這麼一句,我們可以看出,wraps函式其實就是一個修飾器版的update_wrapper函式,它的功能和update_wrapper是一模一樣的。我們可以修改我們上面的自定義修飾器的例子,做出一個更方便閱讀的版本。

自定義修飾器v3

from functools import wraps

def wrapper(f):
    @wraps(f)
    def wrapper_function(*args, **kwargs):
        """這個是修飾函式"""
        return f(*args, **kwargs)
    return wrapper_function
    
@wrapper
def wrapped():
    """這個是被修飾的函式
    """
    print('wrapped')

print(wrapped.__doc__)  # 輸出`這個是被修飾的函式`
print(wrapped.__name__)  # 輸出`wrapped`

至此,我想大家應該明白wraps這個修飾器的作用了吧,就是將 被修飾的函式(wrapped) 的一些屬性值賦值給 修飾器函式(wrapper) ,最終讓屬性的顯示更符合我們的直覺。

參考連結

原博文連結:https://www.cnblogs.com/slysky/p/9777424.html

https://segmentfault.com/a/1190000009398663

https://blog.csdn.net/u010358168/article/details/77773199

  分類: python