python裝飾器進階指南
前言
最近一有時間就在整理自己常用的程式碼片段,並做成了私人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) # ``
經過
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 ```