python裝飾器進階指南

語言: CN / TW / HK

前言

最近一有時間就在整理自己常用的程式碼片段,並做成了私人pip包,正好整理到了裝飾器的部分,所以就想著寫篇文章來總結一下。寫這篇文章的目的是為了讓大家對裝飾器有一個更深入的瞭解,而不是簡單的使用。同時也是自己對裝飾器掌握的一個總結,希望能夠幫助到大家。

需求

我打算帶著實際的需求來看待裝飾器,這樣也會更加容易理解。這道題目也是stackoverflow上的一個問題,我覺得很有意思,所以就拿來做例子。

python {1,2} @make_bold @make_italic def say(): return "Hello"

執行say()將返回: text <b><i>Hello</i></b>

簡單實現

```python

-- coding: utf-8 --

import functools

def make_bold(fn): @functools.wraps(fn) def wrapper(): return "" + fn() + ""

return wrapper

def make_italic(fn): @functools.wraps(fn) def wrapper(): return "" + fn() + ""

return wrapper

@make_bold @make_italic def say(): return "Hello"

if name == 'main': print(say()) ```

看上去挺花哨,其實裝飾器是一個python的語法糖,它的本質就是一個函式,它接受一個函式作為引數,並返回一個函式。我們可以把它理解為一個函式的包裝器,它可以在不改變原函式的基礎上,對函式進行增強。

我們嘗試換種方式表達裝飾器:

```python def say2(): return "Hello"

if name == 'main': print(make_bold(make_italic(say2))()) # Hello ```

可以發現,裝飾器的作用就是把函式say2包裝成了make_bold(make_italic(say2)),然後再呼叫它。

類方式裝飾器

除了用函式的方式來實現裝飾器,我們還可以用類的方式來實現裝飾器。只需要實現__call__魔術方法即可。使得類的例項可以像函式一樣被呼叫。

```python

class MakeTag: def init(self, tag): self.tag = tag

def __call__(self, fn):
    @functools.wraps(fn)
    def wrapper():
        return f"<{self.tag}>" + fn() + f"<!--{self.tag}-->"

    return wrapper

make_bold = MakeTag('b') make_italic = MakeTag('i')

@make_bold @make_italic def say3(): return "Hello" ```

什麼是functools.wraps

functools.wraps它也是一個裝飾器,它能把原函式的一些屬性複製到包裝函式中,比如函式名、文件字串、引數列表等。這樣就不會出現一些奇怪的問題,比如我們在say函式上呼叫help(say),會發現它的文件字串是wrapper函式的文件字串,而不是say函式的文件字串。

我們可以測試一下:

```python {3}

不帶functools.wraps

def make_bold(fn): # @functools.wraps(fn) def wrapper(): """wrapper help doc""" return "" + fn() + ""

return wrapper

@make_bold def say(): """say something""" return "Hello"

print(say.name) # wrapper print(say.doc) # wrapper help doc ```

可以發現,在不被wraps裝飾器裝飾的情況下,say函式的__name____doc__屬性都被改變了。

隨後我們再測試一下帶有functools.wraps的情況:

```python {3}

帶functools.wraps

make_bold(fn): @functools.wraps(fn) def wrapper(): """wrapper help doc""" return "" + fn() + ""

return wrapper

@make_bold def say(): """say something""" return "Hello"

print(say.name) # say print(say.doc) # say something ```

因此,我們在編寫裝飾器的時候,最好都加上functools.wraps

裝飾器的引數

我將對上述的函式裝飾器進行改造,使其可以接受引數。

也就是make_tag('b')將會生成make_bold()這樣的形式。

```python def make_tag(tag): def decorator(fn): @functools.wraps(fn) def wrapper(): return f"<{tag}>{fn()}"

    return wrapper

return decorator

@make_tag('b') @make_tag('i') def say(): return "Hello"

print(say())

Hello

```

再有一種場景,現在我們say函式所返回的內容是固定的Hello,我們希望它可以接受引數,比如say('miclon'),這樣就可以返回<b><i>miclon</i></b>

```python @make_tag('b') @make_tag('i') def say(content): return content

print(say('miclon')) ```

如果我直接修改say函式,那麼就會出現問題,因為say函式的引數列表已經發生了變化,而裝飾器的引數列表壓根沒有引數列表,所以這樣的修改是不行的。

TypeError: wrapper() takes 0 positional arguments but 1 was given

為此我需要改進下裝飾器的引數列表,使其可以接受引數。

```python {4,5} def make_tag(tag): def decorator(fn): @functools.wraps(fn) def wrapper(content): return f"<{tag}>{fn(content)}"

    return wrapper

return decorator

```

然而大部分情況下,我們不會這麼"死板"地將裝飾器的引數列表和被裝飾函式的引數列表一一對應,這樣不夠靈活,也不便於程式碼維護。

因此,正確的做法是,我們將裝飾器的引數列表設定為*args, **kwargs,這樣就可以接受任意數量的引數了。換句話說,無論被裝飾的函式有什麼樣的引數,我作為裝飾器,被裝飾函式的引數統統接受,並全部打回被裝飾的函式。

```python {4,5} def make_tag(tag): def decorator(fn): @functools.wraps(fn) def wrapper(args, kwargs): return f"<{tag}>{fn(args, **kwargs)}"

    return wrapper

return decorator

```

另類的裝飾器

在眾所周知的Django框架中,有一個這樣的裝飾器:

```python class classproperty: def init(self, method=None): self.fget = method

def __get__(self, instance, cls=None):
    return self.fget(cls)

```

它的作用是,可以將一個類方法變成一個類屬性,並且不再需要例項化物件後才可以呼叫。比如:

```python class Demo:

@property
def abc(self):
    return 123

print(Demo.abc) # print(Demo().abc) # 123 `` 經過property的裝飾,我們需要例項化物件後才可以呼叫abc`屬性。

那能不能不例項化也能呼叫呢?也就是換個思路,我得把abc變成一個"類屬性",而不是例項屬性。

我們回到上面classproperty裝飾器上。

乍一看發現它並沒有我上述所說的類裝飾器的特性,它並沒有實現__call__方法,那麼它是如何實現裝飾器的呢?

其實它這個classproperty類中的__get__方法是python中的描述符,用於代理另外一個類的屬性,被裝飾類方法首先會經過__init__,將類方法儲存起來,然後再經過__get__,將類方法代理到類屬性上。一旦屬性被獲取,就會觸發__get__方法,通過self.fget(cls)呼叫類方法。

```python {11} class classproperty: def init(self, method=None): self.fget = method

def __get__(self, instance, cls=None):
    return self.fget(cls)

class Demo:

@classproperty
def abc(self):
    return 123

print(Demo.abc) # 123 ```

它將等同於: ```python class Demo:

def _abc(self): return 123

abc = classproperty(_abc)

print(Demo.abc) # 123 ```