1. 程式人生 > >python中裝飾器詳解

python中裝飾器詳解

裝飾器

我們知道,在python中,我們可以像使用變數一樣使用函,這主要依賴於以下幾點:

  • 函式可以被賦值給其他變數
  • 函式可以被刪除
  • 可以在函式裡面再定義函式,函式巢狀。
  • 函式可以作為引數傳遞給另外一個函式
  • 函式可以作為另一個函式的返回值

對一個簡單的函式進行裝飾
為了更好的理解裝飾器,我們先從一個簡單的例子開始,假設有下面這個函式:

def hello():
    return "hello world!"

現在我們的需求是要增強hello()函式的功能,希望給返回加上HTML標籤,比如<i>hello world</i>,但要求我們不得改變hello()函式原來的定義。

def makeitalic(fun):
    def wrapped():
        return "<i>"+fun()+"</i>"
    return wrapped

在上面的程式碼中,我們定義了一個函式makeitalic,該函式有一個引數fun,它是一個函式;在makeitalic函式裡面我們又定義了一個內部函式wrapped,並將該函式作為返回。

現在我們就達到了我們的需求,不改變hello()函式的定義,但實現了我們想要的功能。

def makeitalic(fun):
          def wrapped():
                    return
"<i>" + fun() + "</i>" return wrapped def hello(): return "hello world" hello = makeitalic(hello) print(hello()) #<i>hello world</i>

在上面,我們將hello函式傳入makeitalic,再將返回賦值給hello,此時,呼叫hello()函式就可以得到我們想要的結果。
不過需要注意的是,由於我們將makeitalic的返回賦值給hello,此時,hello()函式仍然存在,但它已不在指向原來定義的hello()函數了,而是指向了wrapped。
這裡寫圖片描述

現在我們來總結一下,我們上面例子。
為了增強原函式hello的功能,我們定義了一個函式,它接收原函式作為引數,並返回一個新的函式,在這個返回的函式中,執行了原函式,並對原函式的功能進行了增強。完整的程式碼如下所示:

def makeitalic(fun):
          def wrapped():
                    return "<i>" + fun() + "</i>"
          return wrapped


def hello():
          return "hello world"

hello = makeitalic(hello)
print(hello())
print(hello.__name__)

事實上,makeitalic就是一個裝飾器(decorator),它封裝了原函式hello,並返回了一個新函式,用於增強原函式的功能,並將其賦值給hello。

一般情況下,我們使用裝飾器提供的@語法糖(Syntactic Sugar),來簡化上面的操作。

def makeitalic(fun):
    def wrapped():
        return "<i>" + fun() + "</i>"
    return wrapped

@makeitalic
def hello():
    return "hello world"

這種做法是我們在平時寫程式時,常見的操作,但前面例子中的講解才是內部的實現原理。

像上面的情況,可以動態的修改函式(或類的)功能的函式就是裝飾器。本質上,它是一個高階函式,以被裝飾的函式(比如上面的hello)為引數,並返回一個包裝後的函式(比如上面的wrapped)給被修飾函式(hello)。
這裡寫圖片描述

當我們呼叫hello()函式時,hello函式的執行流程如下分析:

  • 把hello函式作為引數傳給@符號後面的裝飾器函式。
  • 然後開始執行裝飾器函式,並返回一個包裝了的函式,同時,改變原函式的指向,現在原函式指向了這個包裝函式。
  • 執行原函式,其實此時執行的是包裝了的函式,所以說,裝飾器增強了一個現有函式的功能,但不會改變現有函式的定義。

裝飾器的使用形式

  • 裝飾器的一般使用形式如下:

    @decorator
    def fun():
     pass
    

    等價於下面的形式

    def fun():
     pass
    fun = decorator(fun)
  • 裝飾器可以定義多個,離函式定義最近的裝飾器最先被呼叫,比如:

    @decotator_one
    @decorator_two
    def fun():
     pass

    等價於:

    def fun():
     pass
    fun = decorator_one(decorator_two(fun)) 
  • 裝飾器還可以帶引數,比如:

    @decorator(arg1, arg2)
    def fun():
     pass

    等價於

    def fun():
     pass
    fun = decorator(arg1, arg2)(fun)

    對帶引數的函式進行修飾
    前面的例子中,被裝飾的函式hello()是沒有帶引數的。現在我們來看被裝飾函式帶引數的情況。

def makeitalic(func):
    def wrapped(*args, **kwargs):
        ret = func(*args, **kwargs)
        return '<i>' + ret + '</i>'
    return wrapped

@makeitalic
def hello(name):
    return 'hello %s' % name

@makeitalic
def hello2(name1, name2):
    return 'hello %s, %s' % (name1, name2)

注意看:內嵌包裝函式的引數傳給了fun,即被修飾的函式,也就是說內嵌包裝函式跟被修飾函式的引數對應,這裡使用了(*args,**kwargs),是為了適應可變引數。
這裡寫圖片描述

帶引數的裝飾器

上面的例子中,我們增強了函式hello的功能,給它的返回加上了<i>標籤,但是現在,如果我們想改用標籤<b>或者<p>。是不是還需要重新定義一個裝飾器呢?其實我們可以在定義一個函式,將標籤作為引數,然後返回一個裝飾器。

def wrap_in_tag(tag):
    def decorator(fun):
        def wrapped(*args, **kwargs):
            # ret此時是原函式返回的字串
            ret = fun(*args, **kwargs)
            return '<' + tag + '>' + ret + '</' + tag + '>'
        return wrapped
    return decorator

makebold = wrap_in_tag("b")

@makebold
def hello(name):
    return "hello %s" % name

print(hello("world"))

這就是帶引數的裝飾器,其實就是在裝飾器外面多了一層包裝,根據不同的引數返回不同的裝飾器。

多個裝飾器

現在,我們來看看多個裝飾器的例子,為了簡單,我們使用不帶引數的裝飾器。

def makebold(fun):
          def wrapped():
                    return "<b>" + fun() + "</b>"

          return wrapped

def makeitalic(fun):
          def wrapped():
                    return "<i>" + fun() + "</i>"

          return wrapped

@makebold
@makeitalic
def hello():
          return "hello world"

print(hello()) # 輸出<b><i>hello world</i></b>

上面定義了兩個裝飾器,對hello進行裝飾,上面的最後幾行程式碼相當於:

def hello():
    return 'hello world'

hello = makebold(makeitalic(hello))

裝飾器的副作用

前面提到過,使用裝飾器有一個瑕疵,就是被修飾的函式,它的函式名稱已經不是原來的名稱了,因為最開始的名稱指向了包裝後的函式。

def makeitalic(func):
    def wrapped():
        return "<i>" + func() + "</i>"
    return wrapped

@makeitalic
def hello():
    return 'hello world'

函式hello被makeitalic裝飾後,它的函式名稱已經改變了。

>>> hello.__name__
'wrapped'

為了消除這樣的副作用,Python中的functions包提供了一個wraps的裝飾器。
這裡寫圖片描述

很好奇wraps內部是怎麼實現的?明天搞吧,今天心情不好。想靜靜。
看了下functools這個模組的原始碼,但是看的不怎麼懂。我猜測這個wraps函式裡面包裝了一個裝飾器,並且最終把這個裝飾器返回了。所以說這個wraps只是外層的包裝函式,並不是真正的裝飾器。

基於類的裝飾器

前面我們一直討論的裝飾器都是一個函式,其實也可以定義基於類的裝飾器。

class Bold(object):
          def __init__(self, fun):
                    self.fun = fun

          def __call__(self, *args, **kwargs):
                    return "<b>" + self.fun(*args, **kwargs) + "</b>"


@Bold
def hello(name):
          return "hello %s" % name

result = hello("world")
print(result)

在利用類定義裝飾器時,我們用到了兩個魔法方法。
__init__():用於初始化一個例項物件,它接收一個函式作為引數,也就是被裝飾的函式。
__call()__:讓一個例項物件可呼叫,就像函式呼叫一樣,在呼叫被裝飾函式時被呼叫。

我們嘗試分析上述程式碼的執行機制:在上面我們用一個類去修飾一個函式,當我們呼叫hello函式時。它作為引數被傳給類Bold,進行了一次例項化的過程,並且新產生的例項也叫hello。這樣我們就可以像掉用函式一樣來呼叫這個例項物件。那麼,對於這種無引數的類裝飾器,其實整個類被稱為一個裝飾器。

下面我們分析帶引數的類裝飾器:

class Tag(object):
          def __init__(self, tag):
                    self.tag = tag

          def __call__(self, fun):
                    def wrapped(*args, **kwargs):
                              return "<{tag}>{res}</{tag}>".format(
                                                  res = fun(*args, **kwargs), tag = self.tag
                                        )
                    return wrapped

@Tag("b")
def hello(name):
          return "hello %s" % name

result = hello("world")
print(result)

需要注意的是,如果類裝飾器有引數,則__init__接收引數,而__call__接收fun
下面我們來分析上述程式碼的執行機制:Tag("b")這個表示式返回的是一個可呼叫物件,雖然是一個例項物件,但可以看做是一個函式,因為實現了__call__()方法。這就回歸到了帶引數的裝飾器情況(用函式定義的)。在這就意味著用一個可呼叫物件去裝飾一個函式。也就是hello = 例項物件(hello),這是上面這個程式碼執行的實質。那麼問題在於呼叫物件()是相當於在呼叫__call__方法嗎?如果有hello = 例項物件(hello),那麼此時hello指向了wrapped函式。呼叫hello()相當於呼叫wrapped()函式。 所以說在帶引數的類裝飾其器中,這個例項物件或者說這個__call__方法才是真正的裝飾器。

總結

  • 本質上,裝飾器就是一個返回函式的高階函式,接收被裝飾函式為引數,然後返回一個包裝了的函式,並自動用被裝飾函式指向返回的這個包裝函式。這一切都歸功於python提供的語法糖(@符號)。
  • 裝飾器可以動態的修改一個類或函式的功能,通過在原有的類或者函式上包裹一層修飾類或者修飾函式實現,不會直接去改變原有類或者函式的定義,這符合軟體開發中的開閉原則(對拓展是開發的,對修改是封閉的)。
  • 事實上,裝飾器就是閉包的一種應用,但它比較特別,接收被修飾函式作為引數,並返回一個函式,賦值給被修飾函式,而閉包則沒有這種限制。