python修飾器(裝飾器)以及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://segmentfault.com/a/1190000009398663

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM