python 中的裝飾器是咋回事

python 中的裝飾器是咋回事

剛接觸裝飾器,會覺得代碼不多卻難以理解。其實裝飾器的語法本身挺簡單的,複雜是因為同時混雜了其它的概念。下面我們一起拋去無關概念,簡單地理解下Python的裝飾器。

裝飾器的原理

在解釋器下跑個裝飾器的例子,直觀地感受一下。

# make_bold就是裝飾器,實現方式這裡略去

>>> @make_bold

... def get_content():

... return 'hello world'

...

>>> get_content()

'hello world'

被make_bold裝飾的get_content,調用後返回結果會自動被b標籤包住。怎麼做到的呢,簡單4步就能明白了。

1. 函數是對象

我們定義個get_content函數。這時get_content也是個對象,它能做所有對象的操作。

def get_content():

return 'hello world'

它有id,有type,有值。

>>> id(get_content)

140090200473112

>>> type(get_content)

<class 'function'>

>>> get_content

<function get_content at 0x7f694aa2be18>

跟其他對象一樣可以被賦值給其它變量。

>>> func_name = get_content

>>> func_name()

'hello world'

它可以當參數傳遞,也可以當返回值

>>> def foo(bar):

... print(bar())

... return bar

...

>>> func = foo(get_content)

hello world

>>> func()

'hello world'

2. 自定義函數對象

我們可以用class來構造函數對象。有成員函數__call__的就是函數對象了,函數對象被調用時正是調用的__call__。

class FuncObj(object):

def __init__(self, name):

print('Initialize')

self.name= name

def __call__(self):

print('Hi', self.name)

我們來調用看看。可以看到,函數對象的使用分兩步:構造和調用(同學們注意了,這是考點)。

>>> fo = FuncObj('python')

Initialize

>>> fo()

Hi python

3. @是個語法糖

裝飾器的@沒有做什麼特別的事,不用它也可以實現一樣的功能,只不過需要更多的代碼。

@make_bold

def get_content():

return 'hello world'

# 上面的代碼等價於下面的

def get_content():

return 'hello world'

get_content = make_bold(get_content)

make_bold是個函數,要求入參是函數對象,返回值是函數對象。@的語法糖其實是省去了上面最後一行代碼,使可讀性更好。用了裝飾器後,每次調用get_content,真正調用的是make_bold返回的函數對象。

4. 用類實現裝飾器

入參是函數對象,返回是函數對象,如果第2步裡的類的構造函數改成入參是個函數對象,不就正好符合要求嗎?我們來試試實現make_bold。

class make_bold(object):

def __init__(self, func):

print('Initialize')

self.func = func

def __call__(self):

print('Call')

return '{}'.format(self.func())

大功告成,看看能不能用。

>>> @make_bold

... def get_content():

... return 'hello world'

...

Initialize

>>> get_content()

Call

'hello world'

成功實現裝飾器!是不是很簡單?

這裡分析一下之前強調的構造和調用兩個過程。我們去掉@語法糖好理解一些。

# 構造,使用裝飾器時構造函數對象,調用了__init__

>>> get_content = make_bold(get_content)

Initialize

# 調用,實際上直接調用的是make_bold構造出來的函數對象

>>> get_content()

Call

'hello world'

到這裡就徹底清楚了,完結撒花,可以關掉網頁了~~~(如果只是想知道裝飾器原理的話)

函數版裝飾器

閱讀源碼時,經常見到用嵌套函數實現的裝飾器,怎麼理解?同樣僅需4步。

1. def的函數對象初始化

用class實現的函數對象很容易看到什麼時候構造的,那def定義的函數對象什麼時候構造的呢?

# 這裡的全局變量刪去了無關的內容

>>> globals()

{}

>>> def func():

... pass

...

>>> globals()

{'func': <function func at 0x10f5baf28>}

不像一些編譯型語言,程序在啟動時函數已經構造那好了。上面的例子可以看到,執行到def會才構造出一個函數對象,並賦值給變量make_bold。

這段代碼和下面的代碼效果是很像的。

class NoName(object):

def __call__(self):

pass

func = NoName()

2. 嵌套函數

Python的函數可以嵌套定義。

def outer():

print('Before def:', locals())

def inner():

pass

print('After def:', locals())

return inner

inner是在outer內定義的,所以算outer的局部變量。執行到def inner時函數對象才創建,因此每次調用outer都會創建一個新的inner。下面可以看出,每次返回的inner是不同的。

>>> outer()

Before def: {}

After def: {'inner': <function outer..inner at 0x7f0b18fa0048>}

<function outer..inner at 0x7f0b18fa0048>

>>> outer()

Before def: {}

After def: {'inner': <function outer..inner at 0x7f0b18fa00d0>}

<function outer..inner at 0x7f0b18fa00d0>

3. 閉包

嵌套函數有什麼特別之處?因為有閉包。

def outer():

msg = 'hello world'

def inner():

print(msg)

return inner

下面的試驗表明,inner可以訪問到outer的局部變量msg。

>>> func = outer()

>>> func()

hello world

閉包有2個特點

  1. inner能訪問outer及其祖先函數的命名空間內的變量(局部變量,函數參數)。
  2. 調用outer已經返回了,但是它的命名空間被返回的inner對象引用,所以還不會被回收。

這部分想深入可以去了解Python的LEGB規則。

4. 用函數實現裝飾器

裝飾器要求入參是函數對象,返回值是函數對象,嵌套函數完全能勝任。

def make_bold(func):

print('Initialize')

def wrapper():

print('Call')

return '{}'.format(func())

return wrapper

用法跟類實現的裝飾器一樣。可以去掉@語法糖分析下構造和調用的時機。

>>> @make_bold

... def get_content():

... return 'hello world'

...

Initialize

>>> get_content()

Call

'hello world'

因為返回的wrapper還在引用著,所以存在於make_bold命名空間的func不會消失。make_bold可以裝飾多個函數,wrapper不會調用混淆,因為每次調用make_bold,都會有創建新的命名空間和新的wrapper。

到此函數實現裝飾器也理清楚了,完結撒花,可以關掉網頁了~~~(後面是使用裝飾的常見問題)

常見問題

1. 怎麼實現帶參數的裝飾器?

帶參數的裝飾器,有時會異常的好用。我們看個例子。

>>> @make_header(2)

... def get_content():

... return 'hello world'

...

>>> get_content()

'

hello world

'

怎麼做到的呢?其實這跟裝飾器語法沒什麼關係。去掉@語法糖會變得很容易理解。

@make_header(2)

def get_content():

return 'hello world'

# 等價於

def get_content():

return 'hello world'

unnamed_decorator = make_header(2)

get_content = unnamed_decorator(get_content)

上面代碼中的unnamed_decorator才是真正的裝飾器,make_header是個普通的函數,它的返回值是裝飾器。

來看一下實現的代碼。

def make_header(level):

print('Create decorator')

# 這部分跟通常的裝飾器一樣,只是wrapper通過閉包訪問了變量level

def decorator(func):

print('Initialize')

def wrapper():

print('Call')

return '{1}'.format(level, func())

return wrapper

# make_header返回裝飾器

return decorator

看了實現代碼,裝飾器的構造和調用的時序已經很清楚了。

>>> @make_header(2)

... def get_content():

... return 'hello world'

...

Create decorator

Initialize

>>> get_content()

Call

'

hello world

'

2. 如何裝飾有參數的函數?

為了有條理地理解裝飾器,之前例子裡的被裝飾函數有意設計成無參的。我們來看個例子。

@make_bold

def get_login_tip(name):

return 'Welcome back, {}'.format(name)

最直接的想法是把get_login_tip的參數透傳下去。

class make_bold(object):

def __init__(self, func):

self.func = func

def __call__(self, name):

return

'{}'.format(self.func(name))

如果被裝飾的函數參數是明確固定的,這麼寫是沒有問題的。但是make_bold明顯不是這種場景。它既需要裝飾沒有參數的get_content,又需要裝飾有參數的get_login_tip。這時候就需要可變參數了。

class make_bold(object):

def __init__(self, func):

self.func = func

def __call__(self, *args, **kwargs):

return '{}'.format(self.func(*args, **kwargs))

當裝飾器不關心被裝飾函數的參數,或是被裝飾函數的參數多種多樣的時候,可變參數非常合適。可變參數不屬於裝飾器的語法內容,這裡就不深入探討了。

3. 一個函數能否被多個裝飾器裝飾?

下面這麼寫合法嗎?

@make_italic

@make_bold

def get_content():

return 'hello world'

合法。上面的的代碼和下面等價,留意一下裝飾的順序。

def get_content():

return 'hello world'

get_content = make_bold(get_content) # 先裝飾離函數定義近的

get_content = make_italic(get_content)

4. functools.wraps有什麼用?

Python的裝飾器倍感貼心的地方是對調用方透明。調用方完全不知道也不需要知道調用的函數被裝飾了。這樣我們就能在調用方的代碼完全不改動的前提下,給函數patch功能。

為了對調用方透明,裝飾器返回的對象要偽裝成被裝飾的函數。偽裝得越像,對調用方來說差異越小。有時光偽裝函數名和參數是不夠的,因為Python的函數對象有一些元信息調用方可能讀取了。為了連這些元信息也偽裝上,functools.wraps出場了。它能用於把被調用函數的__module__,__name__,__qualname__,__doc__,__annotations__賦值給裝飾器返回的函數對象。

import functools

def make_bold(func):

@functools.wraps(func)

def wrapper(*args, **kwargs):

return '{}'.format(func(*args, **kwargs))

return wrapper

對比一下效果。

>>> @make_bold

... def get_content():

... '''Return page content'''

... return 'hello world'

# 不用functools.wraps的結果

>>> get_content.__name__

'wrapper'

>>> get_content.__doc__

>>>

# 用functools.wraps的結果

>>> get_content.__name__

'get_content'

>>> get_content.__doc__

'Return page content'

實現裝飾器時往往不知道調用方會怎麼用,所以養成好習慣加上functools.wraps吧。


分享到:


相關文章: