python 裝飾器基本原理

定義

關於python中裝飾器的定義,我們這裏參考廖雪峯大神的python3教程中的定義:在某個函數(代碼)運行期間,在不更改該函數的功能下,動態給該函數添加功能的方式,我們稱之爲“裝飾器”。從定義中看出,這個裝飾器勢必要在實現中傳入原函數,並在其功能中使原函數功能不受影響。由此我們想到的一種實現裝飾器的方法就是:“實現一個功能(可是一段代碼/函數),該功能的實現要傳入一個函數。”翻譯過來就是通過一個函數(比如叫“a”函數)實現裝飾器的功能,但是該函數a的入參需要傳入的是一個函數(比如叫“b”函數)。那麼想象中的裝飾器實現方式是:

# 這裏的b是一個函數
def a(b):
   b()
   pass

而實際上python中基本的裝飾器的模樣是這樣(注:文章中的大量定義和源碼都參考廖雪峯大神的python教程):

# 這裏的func是一個函數,與想象中的b函數一致,log就是上面中的a函數
def log(func):
# *args,**kw泛指函數func的入參
   def wrapper(*args, **kw):
       # __name__是python中的一個屬性,它能返回對應函數的名稱
       print('call %s():' % func.__name__)
       return func(*args, **kw)
   return wrapper

要使用裝飾器,python中的方法如下:

# 給函數上面加一個@符號,再接實現了裝飾器功能的函數名稱
@log
def now():
   print('2015-3-25')

調用now()函數:

>>> now()
call now():
2015-3-25

可以看到使用了裝飾器,給now函數動態增加了打印它的名稱的功能。且沒有影響它的原始功能。
根據對比,python中裝飾器的實現跟想象中的實現方式差不多,不過python中實現裝飾器的函數log其返回值是一個函數wrapper,這個log是一個 返回函數 ,什麼是返回函數?爲了弄清楚這個log函數,我們還是先來看一些函數的基礎知識吧

基礎知識

函數也是一種對象

在python中,“函數也是一種對象”,這句話是理解裝飾器的基礎。因爲函數是一種對象,並且對象能被賦值給變量,所以函數就能被賦值給變量。例如:

>>> def now():
...     print('2015-3-25')
...
>>> f = now
>>> f()

這裏now函數被賦值給了f變量,調用now函數,其實就是進行f的調用。平時來看,函數一般都有入參(即函數的局部變量),而因爲變量能代表函數,故函數入參爲函數也就好理解了。故對實現了裝飾器的函數,它的入參爲函數也就不奇怪了。在python中當一個函數a就將另一個函數b作爲參數,那麼這個函數a就稱之爲 高階函數 。而上面的函數log將func函數作入參,那麼log就是一個高階函數。

返回函數

可以看到函數log它最終的返回值是一個函數wrapper。在python中,返回函數指:“對於一個函數c,它最後的返回值是一個函數d,那麼函數c就是一個返回函數”。返回函數的好處就是當調用c函數時不用立即執行c中的所有邏輯,我們可以看下面的例子:

def lazy_sum(*args):
    def sum():
        ax = 0
        for n in args:
            ax = ax + n
        return ax
    return sum

函數lazy_sum實現了一個求和的功能,當我們調用lazy_sum時,其返回的不是直接的求和值,而是一個函數sum,

>>> f = lazy_sum(1, 3, 5, 7, 9)
>>> f
<function lazy_sum.<locals>.sum at 0x101c6ed90>

這裏f變量其實就是sum函數,所以要求和的值時,調用f纔可以。

>>> f()
25

“當有一個函數h,它定義了內部函數g,最後返回了函數g,並且在該過程中g還使用了h函數的局部變量,那麼這種程序python中稱之爲 閉包 ”。根據閉包定義,其實它也是一種返回函數,只不過它的內部函數使用了外部函數的局部變量(入參)。

裝飾器理解

裝飾器剖析

有了前面的知識儲備,再來看最開始實現了裝飾器功能的函數log:

def log(func):
    def wrapper(*args, **kw):
        print('call %s():' % func.__name__)
        return func(*args, **kw)
    return wrapper

可以看到log函數的入參爲func函數,根據定義,log函數就是一個高階函數。再看log函數的返回值爲函數wrapper,根據定義,log函數也是一個返回函數。如果把func看作一個變量,那麼內部的函數wrapper使用了外部函數log的變量,根據定義,這個log函數它的程序也是一個閉包。
此刻我們對於實現裝飾器功能的程序就有了一個整體認識,它既是一個高階函數也是一個返回函數。當然整體上看它也是一個閉包。

裝飾器的運行過程

在python中,當我們使用裝飾器:

@log
def now():
    print('2015-3-25')


now()

now()運行過程,實際上相當於執行了語句:

now = log(now)

用了一個同原來now函數名字相同的變量now,(爲了好區分,我們後面稱變量now爲now’吧。)來表示log函數的返回值。 這裏log函數的返回值是函數wrapper,前面講過函數是變量,變量能表示函數,此時now’變量表示了函數wrapper,即 now’ = wrapper ,now()的執行相當於執行了wrapper函數,即 now’()
表面上看加了裝飾器的now函數,其調用跟不加裝飾器正常調用now函數是一樣的,但經過上面我分析實際上並並不是,它實際上執行的是 now’ = log(now); now’() 這兩步。那句廣告語“看起來一樣一樣,實際就是不一樣…”,哈哈,說的就是這個過程。

帶參數的裝飾器

有時候我們給某個函數動態添加了功能,但又希望我們添加的功能對於我們的需求是可定製的。因此則有必要給裝飾器添加參數變量。python中給裝飾器添加參數的實現是這樣的:

def log2(text):
    def log(func):
        def wrapper(*args, **kw):
            print('%s %s():' % (text, func.__name__))
            return func(*args, **kw)
        return wrapper
    return log

用法則如下:

@log2('execute')
def now():
    print('2015-3-25')
>>> now()
execute now():
2015-3-25

直觀上看,其實就是給log函數外部再加了一層函數。根據前面的函數定義,不難看出log2是一個返回函數。而now()的運行過程則相當於執行了下面的語句:

now = log2('execute')(now)

用了一個同原來now函數名字相同的變量now,(爲了好區分,我們也稱變量now爲now’吧。)來表示log函數的返回值。這裏爲什麼now’表示的是log函數的返回值,而不是log2的返回值?相信聰明的你一眼也能得到答案哈!我們看,log2(‘execute’) 這個函數的執行,實際上是返回了log函數,我們假設將返回的log函數賦值給變量f,那麼 log2(‘execute’)(now) 的執行其實就相當於 f(now) 的執行,而 f(now) 的執行,其返回值就是log函數的返回值即wrapper函數。所以這裏now’其實表示的是wrapper函數。
可以看到無論是帶參數還是不帶參的裝飾器,其表面上看now()的執行是執行了now,實際上都是執行wrapper函數。不相信的小夥伴可以打印下now的名稱(即now.__name__)試一下,其值都是wrapper。表面上看python將now()的執行僞裝了,但終究“狐狸的尾巴藏不住啊”,打印下它的函數名稱,它就顯露原形了。那麼爲了在平時使用中保持一致性,即加不加裝飾器,打印“now.__name__”時都可以得到now函數的原始名字now’,python也給了我們方法,即使用Python內置的functools.wraps即可。不帶參和帶參的方法各如下:

import functools

def log(func):
    @functools.wraps(func)
    def wrapper(*args, **kw):
        print('call %s():' % func.__name__)
        return func(*args, **kw)
    return wrapper
import functools

def log2(text):
    def log(func):
        @functools.wraps(func)
        def wrapper(*args, **kw):
            print('%s %s():' % (text, func.__name__))
            return func(*args, **kw)
        return wrapper
    return log

可以看到,其@functools.wraps(func)語句都是在wrapper函數上面加的。

總結

當然python的裝飾器使用不止這些。這裏只是最基本的理解(若有理解錯的,請各位大佬指正哈)。對裝飾器的理解首先是要知道函數也是對象,其次只要弄清楚一些函數定義就可以了。當然這裏還是得感謝廖雪峯大神的python3教程,正是因爲他那清晰透徹的教程,才使我理解裝飾器的概念。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章