Python: 會打扮的裝飾器

裝飾器

咱們知道,在 Python 中,咱們能夠像使用變量同樣使用函數:html

  • 函數能夠被賦值給其餘變量java

  • 函數能夠被刪除python

  • 能夠在函數裏面再定義函數shell

  • 函數能夠做爲參數傳遞給另一個函數編程

  • 函數能夠做爲另外一個函數的返回閉包

簡而言之,函數就是一個對象app

對一個簡單的函數進行裝飾

爲了更好地理解裝飾器,咱們先從一個簡單的例子開始,假設有下面的函數:函數式編程

def hello():
    return 'hello world'

如今咱們想加強 hello() 函數的功能,但願給返回加上 HTML 標籤,好比 <i>hello world</i>,可是有一個要求,不改變原來 hello() 函數的定義。這裏固然有不少種方法,下面給出一種跟本文相關的方法:函數

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

在上面的代碼中,咱們定義了一個函數 makeitalic,該函數有一個參數 func,它是一個函數;在 makeitalic 函數裏面咱們又定義了一個內部函數 wrapped,並將該函數做爲返回。.net

如今,咱們就能夠不改變 hello() 函數的定義,給返回加上 HTML 標籤了:

>>> hello = makeitalic(hello)  # 將 hello 函數傳給 makeitalic
>>> hello()
'<i>hello world</i>'

在上面,咱們將 hello 函數傳給 makeitalic,再將返回賦給 hello,此時調用 hello() 就獲得了咱們想要的結果。

不過要注意的是,因爲咱們將 makeitalic 的返回賦給了 hello,此時 hello() 函數仍然存在,可是它的函數名再也不是 hello 了,而是 wrapped,正是 makeitalic 返回函數的名稱,能夠驗證一下:

>>> hello.__name__
'wrapped'

對於這個小瑕疵,後文將會給出解決方法。

如今,咱們梳理一下上面的例子,爲了加強原函數 hello 的功能,咱們定義了一個函數,它接收原函數做爲參數,並返回一個新的函數,完整的代碼以下:

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

def hello():
    return 'hello world'

hello = makeitalic(hello)

事實上,makeitalic 就是一個裝飾器(decorator),它『裝飾』了函數 hello,並返回一個函數,將其賦給 hello

通常狀況下,咱們使用裝飾器提供的 @ 語法糖(Syntactic Sugar),來簡化上面的寫法:

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

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

像上面的狀況,能夠動態修改函數(或類)功能的函數就是裝飾器。本質上,它是一個高階函數,以被裝飾的函數(好比上面的 hello)爲參數,並返回一個包裝後的函數(好比上面的 wrapped)給被裝飾函數(hello)

裝飾器的使用形式

  • 裝飾器的通常使用形式以下:

@decorator
def func():
    pass

等價於下面的形式:

def func():
    pass
func = decorator(func)
  • 裝飾器能夠定義多個,離函數定義最近的裝飾器先被調用,好比:

@decorator_one
@decorator_two
def func():
    pass

等價於:

def func():
    pass

func = decorator_one(decorator_two(func))
  • 裝飾器還能夠帶參數,好比:

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

等價於:

def func():
    pass

func = decorator(arg1, arg2)(func)

下面咱們再看一些具體的例子,以加深對它的理解。

對帶參數的函數進行裝飾

前面的例子中,被裝飾的函數 hello() 是沒有帶參數的,咱們看看被裝飾函數帶參數的狀況。對前面例子中的 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)

因爲函數 hello 帶參數,所以內嵌包裝函數 wrapped 也作了一點改變:

  • 內嵌包裝函數的參數傳給了 func,即被裝飾函數,也就是說內嵌包裝函數的參數跟被裝飾函數的參數對應,這裏使用了 (*args, **kwargs),是爲了適應可變參數。

看看使用:

>>> hello('python')
'<i>hello python</i>'
>>> hello2('python', 'java')
'<i>hello python, java</i>'

帶參數的裝飾器

上面的例子,咱們加強了函數 hello 的功能,給它的返回加上了標籤 <i>...</i>,如今,咱們想改用標籤 <b>...</b><p>...</p>。是否是要像前面同樣,再定義一個相似 makeitalic 的裝飾器呢?其實,咱們能夠定義一個函數,將標籤做爲參數,返回一個裝飾器,好比:

def wrap_in_tag(tag):
    def decorator(func):
        def wrapped(*args, **kwargs):
            ret = func(*args, **kwargs)
            return '<' + tag + '>' + ret + '</' + tag + '>'
        return wrapped

    return decorator

如今,咱們能夠根據須要生成想要的裝飾器了:

makebold = wrap_in_tag('b')  # 根據 'b' 返回 makebold 生成器

@makebold
def hello(name):
    return 'hello %s' % name
    
>>> hello('world')
'<b>hello world</b>'

上面的形式也能夠寫得更加簡潔:

@wrap_in_tag('b')
def hello(name):
    return 'hello %s' % name

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

多個裝飾器

如今,讓咱們來看看多個裝飾器的例子,爲了簡單起見,下面的例子就不使用帶參數的裝飾器。

def makebold(func):
    def wrapped():
        return '<b>' + func() + '</b>'

    return wrapped

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

    return wrapped

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

上面定義了兩個裝飾器,對 hello 進行裝飾,上面的最後幾行代碼至關於:

def hello():
    return 'hello world'

hello = makebold(makeitalic(hello))

調用函數 hello

>>> hello()
'<b><i>hello world</i></b>'

基於類的裝飾器

前面的裝飾器都是一個函數,其實也能夠基於類定義裝飾器,看下面的例子:

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

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

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

>>> hello('world')
'<b>hello world</b>'

能夠看到,類 Bold 有兩個方法:

  • __init__():它接收一個函數做爲參數,也就是被裝飾的函數

  • __call__():讓類對象可調用,就像函數調用同樣,在調用被裝飾函數時被調用

還可讓類裝飾器帶參數:

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

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

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

須要注意的是,若是類裝飾器有參數,則 __init__ 接收參數,而 __call__ 接收 func

裝飾器的反作用

前面提到,使用裝飾器有一個瑕疵,就是被裝飾的函數,它的函數名稱已經不是原來的名稱了,回到最開始的例子:

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

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

函數 hellomakeitalic 裝飾後,它的函數名稱已經改變了:

>>> hello.__name__
'wrapped'

爲了消除這樣的反作用,Python 中的 functool 包提供了一個 wraps 的裝飾器:

from functools import wraps

def makeitalic(func):
    @wraps(func)       # 加上 wraps 裝飾器
    def wrapped():
        return "<i>" + func() + "</i>"
    return wrapped

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

>>> hello.__name__
'hello'

小結

  • 本質上,裝飾器就是一個返回函數的高階函數。

  • 裝飾器能夠動態地修改一個類或函數的功能,經過在原有的類或者函數上包裹一層修飾類或修飾函數實現。

  • 事實上,裝飾器就是閉包的一種應用,但它比較特別,接收被裝飾函數爲參數,並返回一個函數,賦給被裝飾函數,閉包則沒這種限制。

本文由 funhacks 發表於我的博客,採用 Creative Commons BY-NC-ND 4.0(自由轉載-保持署名-非商用-禁止演繹)協議發佈。
非商業轉載請註明做者及出處。商業轉載請聯繫做者本人。
本文標題爲: Python: 會打扮的裝飾器
本文連接爲: https://funhacks.net/2016/11/...

參考資料

相關文章
相關標籤/搜索