Python的裝飾器使用詳解
Python有大量強(qiáng)大又貼心的特性,如果要列個最受歡迎排行榜,那么裝飾器絕對會在其中。
初識裝飾器,會感覺到優(yōu)雅且神奇,想親手實(shí)現(xiàn)時卻總有距離感,就像深閨的冰美人一般。這往往是因為理解裝飾器時把其他的一些概念混雜在一起了。待我撫去層層面紗,你會看到純粹的裝飾器其實(shí)蠻簡單直率的。
裝飾器的原理
在解釋器下跑個裝飾器的例子,直觀地感受一下。
# make_bold就是裝飾器,實(shí)現(xiàn)方式這里略去
>>> @make_bold ... def get_content(): ... return 'hello world' ... >>> get_content() '<b>hello world</b>'
被 make_bold 裝飾的 get_content ,調(diào)用后返回結(jié)果會自動被 b 標(biāo)簽包住。怎么做到的呢,簡單4步就能明白了。
1. 函數(shù)是對象
我們定義個 get_content 函數(shù)。這時 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'
它可以當(dāng)參數(shù)傳遞,也可以當(dāng)返回值
>>> def foo(bar): ... print(bar()) ... return bar ... >>> func = foo(get_content) hello world >>> func() 'hello world'
2. 自定義函數(shù)對象
我們可以用 class 來構(gòu)造函數(shù)對象。有成員函數(shù) __call__ 的就是函數(shù)對象了,函數(shù)對象被調(diào)用時正是調(diào)用的 __call__ 。
class FuncObj(object): def __init__(self, name): print('Initialize') self.name= name def __call__(self): print('Hi', self.name)
我們來調(diào)用看看。可以看到, 函數(shù)對象的使用分兩步:構(gòu)造和調(diào)用 (同學(xué)們注意了,這是考點(diǎn))。
>>> fo = FuncObj('python') Initialize >>> fo() Hi python
3. @ 是個語法糖
裝飾器的 @ 沒有做什么特別的事,不用它也可以實(shí)現(xiàn)一樣的功能,只不過需要更多的代碼。
@make_bold def get_content(): return 'hello world' # 上面的代碼等價于下面的 def get_content(): return 'hello world' get_content = make_bold(get_content)
make_bold 是個函數(shù),要求入?yún)⑹呛瘮?shù)對象,返回值是函數(shù)對象。 @ 的語法糖其實(shí)是省去了上面最后一行代碼,使可讀性更好。用了裝飾器后,每次調(diào)用 get_content ,真正調(diào)用的是 make_bold 返回的函數(shù)對象。
4. 用類實(shí)現(xiàn)裝飾器
入?yún)⑹呛瘮?shù)對象,返回是函數(shù)對象,如果第2步里的類的構(gòu)造函數(shù)改成入?yún)⑹莻€函數(shù)對象,不就正好符合要求嗎?我們來試試實(shí)現(xiàn) make_bold 。
class make_bold(object): def __init__(self, func): print('Initialize') self.func = func def __call__(self): print('Call') return '<b>{}</b>'.format(self.func())
大功告成,看看能不能用。
>>> @make_bold ... def get_content(): ... return 'hello world' ... Initialize >>> get_content() Call '<b>hello world</b>'
成功實(shí)現(xiàn)裝飾器!是不是很簡單?
這里分析一下之前強(qiáng)調(diào)的 構(gòu)造 和 調(diào)用 兩個過程。我們?nèi)サ?@ 語法糖好理解一些。
# 構(gòu)造,使用裝飾器時構(gòu)造函數(shù)對象,調(diào)用了__init__
>>> get_content = make_bold(get_content) Initialize # 調(diào)用,實(shí)際上直接調(diào)用的是make_bold構(gòu)造出來的函數(shù)對象 >>> get_content() Call '<b>hello world</b>'
到這里就徹底清楚了,完結(jié)撒花,可以關(guān)掉網(wǎng)頁了~~~(如果只是想知道裝飾器原理的話)
函數(shù)版裝飾器
閱讀源碼時,經(jīng)常見到用嵌套函數(shù)實(shí)現(xiàn)的裝飾器,怎么理解?同樣僅需4步。
1. def 的函數(shù)對象初始化
用 class 實(shí)現(xiàn)的函數(shù)對象很容易看到什么時候 構(gòu)造 的,那 def 定義的函數(shù)對象什么時候 構(gòu)造 的呢?
# 這里的全局變量刪去了無關(guān)的內(nèi)容
>>> globals() {} >>> def func(): ... pass ... >>> globals() {'func': <function func at 0x10f5baf28>}
不像一些編譯型語言,程序在啟動時函數(shù)已經(jīng)構(gòu)造那好了。上面的例子可以看到,執(zhí)行到 def 會才構(gòu)造出一個函數(shù)對象,并賦值給變量 make_bold 。
這段代碼和下面的代碼效果是很像的。
class NoName(object): def __call__(self): pass func = NoName()
2. 嵌套函數(shù)
Python的函數(shù)可以嵌套定義。
def outer(): print('Before def:', locals()) def inner(): pass print('After def:', locals()) return inner
inner 是在 outer 內(nèi)定義的,所以算 outer 的局部變量。執(zhí)行到 def inner 時函數(shù)對象才創(chuàng)建,因此每次調(diào)用 outer 都會創(chuàng)建一個新的 inner 。下面可以看出,每次返回的 inner 是不同的。
>>> outer() Before def: {} After def: {'inner': <function outer.<locals>.inner at 0x7f0b18fa0048>} <function outer.<locals>.inner at 0x7f0b18fa0048> >>> outer() Before def: {} After def: {'inner': <function outer.<locals>.inner at 0x7f0b18fa00d0>} <function outer.<locals>.inner at 0x7f0b18fa00d0>
3. 閉包
嵌套函數(shù)有什么特別之處?因為有閉包。
def outer(): msg = 'hello world' def inner(): print(msg) return inner
下面的試驗表明, inner 可以訪問到 outer 的局部變量 msg 。
>>> func = outer() >>> func() hello world
閉包有2個特點(diǎn)
1. inner 能訪問 outer 及其祖先函數(shù)的命名空間內(nèi)的變量(局部變量,函數(shù)參數(shù))。
2. 調(diào)用 outer 已經(jīng)返回了,但是它的命名空間被返回的 inner 對象引用,所以還不會被回收。
這部分想深入可以去了解Python的LEGB規(guī)則。
4. 用函數(shù)實(shí)現(xiàn)裝飾器
裝飾器要求入?yún)⑹呛瘮?shù)對象,返回值是函數(shù)對象,嵌套函數(shù)完全能勝任。
def make_bold(func): print('Initialize') def wrapper(): print('Call') return '<b>{}</b>'.format(func()) return wrapper
用法跟類實(shí)現(xiàn)的裝飾器一樣??梢匀サ?@ 語法糖分析下 構(gòu)造 和 調(diào)用 的時機(jī)。
>>> @make_bold ... def get_content(): ... return 'hello world' ... Initialize >>> get_content() Call '<b>hello world</b>'
因為返回的 wrapper 還在引用著,所以存在于 make_bold 命名空間的 func 不會消失。 make_bold 可以裝飾多個函數(shù), wrapper 不會調(diào)用混淆,因為每次調(diào)用 make_bold ,都會有創(chuàng)建新的命名空間和新的 wrapper 。
到此函數(shù)實(shí)現(xiàn)裝飾器也理清楚了,完結(jié)撒花,可以關(guān)掉網(wǎng)頁了~~~(后面是使用裝飾的常見問題)
常見問題
1. 怎么實(shí)現(xiàn)帶參數(shù)的裝飾器?
帶參數(shù)的裝飾器,有時會異常的好用。我們看個例子。
>>> @make_header(2) ... def get_content(): ... return 'hello world' ... >>> get_content() '<h2>hello world</h2>'
怎么做到的呢?其實(shí)這跟裝飾器語法沒什么關(guān)系。去掉 @ 語法糖會變得很容易理解。
@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 是個普通的函數(shù),它的返回值是裝飾器。
來看一下實(shí)現(xiàn)的代碼。
def make_header(level): print('Create decorator') # 這部分跟通常的裝飾器一樣,只是wrapper通過閉包訪問了變量level def decorator(func): print('Initialize') def wrapper(): print('Call') return '<h{0}>{1}</h{0}>'.format(level, func()) return wrapper # make_header返回裝飾器 return decorator
看了實(shí)現(xiàn)代碼,裝飾器的 構(gòu)造 和 調(diào)用 的時序已經(jīng)很清楚了。
>>> @make_header(2) ... def get_content(): ... return 'hello world' ... Create decorator Initialize >>> get_content() Call '<h2>hello world</h2>'
2. 如何裝飾有參數(shù)的函數(shù)?
為了有條理地理解裝飾器,之前例子里的被裝飾函數(shù)有意設(shè)計成無參的。我們來看個例子。
@make_bold def get_login_tip(name): return 'Welcome back, {}'.format(name)
最直接的想法是把 get_login_tip 的參數(shù)透傳下去。
class make_bold(object): def __init__(self, func): self.func = func def __call__(self, name): return '<b>{}</b>'.format(self.func(name))
如果被裝飾的函數(shù)參數(shù)是明確固定的,這么寫是沒有問題的。但是 make_bold 明顯不是這種場景。它既需要裝飾沒有參數(shù)的 get_content ,又需要裝飾有參數(shù)的 get_login_tip 。這時候就需要可變參數(shù)了。
class make_bold(object): def __init__(self, func): self.func = func def __call__(self, *args, **kwargs): return '<b>{}</b>'.format(self.func(*args, **kwargs))
當(dāng)裝飾器不關(guān)心被裝飾函數(shù)的參數(shù),或是被裝飾函數(shù)的參數(shù)多種多樣的時候,可變參數(shù)非常合適。可變參數(shù)不屬于裝飾器的語法內(nèi)容,這里就不深入探討了。
3. 一個函數(shù)能否被多個裝飾器裝飾?
下面這么寫合法嗎?
@make_italic @make_bold def get_content(): return 'hello world'
合法。上面的的代碼和下面等價,留意一下裝飾的順序。
def get_content(): return 'hello world' get_content = make_bold(get_content) # 先裝飾離函數(shù)定義近的 get_content = make_italic(get_content)
4. functools.wraps 有什么用?
Python的裝飾器倍感貼心的地方是對調(diào)用方透明。調(diào)用方完全不知道也不需要知道調(diào)用的函數(shù)被裝飾了。這樣我們就能在調(diào)用方的代碼完全不改動的前提下,給函數(shù)patch功能。
為了對調(diào)用方透明,裝飾器返回的對象要偽裝成被裝飾的函數(shù)。偽裝得越像,對調(diào)用方來說差異越小。有時光偽裝函數(shù)名和參數(shù)是不夠的,因為Python的函數(shù)對象有一些元信息調(diào)用方可能讀取了。為了連這些元信息也偽裝上, functools.wraps 出場了。它能用于把被調(diào)用函數(shù)的 __module__ , __name__ , __qualname__ , __doc__ , __annotations__ 賦值給裝飾器返回的函數(shù)對象。
import functools def make_bold(func): @functools.wraps(func) def wrapper(*args, **kwargs): return '<b>{}</b>'.format(func(*args, **kwargs)) return wrapper
對比一下效果。
>>> @make_bold ... def get_content(): ... '''Return page content''' ... return 'hello world' # 不用functools.wraps的結(jié)果 >>> get_content.__name__ 'wrapper' >>> get_content.__doc__ >>> # 用functools.wraps的結(jié)果 >>> get_content.__name__ 'get_content' >>> get_content.__doc__ 'Return page content'
實(shí)現(xiàn)裝飾器時往往不知道調(diào)用方會怎么用,所以養(yǎng)成好習(xí)慣加上 functools.wraps 吧。
這次是真·完結(jié)了,撒花吧~~~
相關(guān)文章
Python sklearn對文本數(shù)據(jù)進(jìn)行特征化提取
這篇文章主要介紹了Python sklearn對文本數(shù)據(jù)進(jìn)行特征化提取,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2023-04-04Python Counting Bloom Filter原理與實(shí)現(xiàn)詳細(xì)介紹
這篇文章主要介紹了Python Counting Bloom Filter原理與實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2022-10-10python 實(shí)現(xiàn)多線程下載視頻的代碼
這篇文章主要介紹了python 實(shí)現(xiàn)多線程下載視頻的代碼,代碼簡單易懂,非常不錯,具有一定的參考借鑒價值,需要的朋友可以參考下2019-11-11Python3 MySQL 數(shù)據(jù)庫連接的使用示例
本文我們?yōu)榇蠹医榻B Python3 使用 PyMySQL 連接數(shù)據(jù)庫,并實(shí)現(xiàn)簡單的增刪改查,需要的朋友可以參考下2021-06-06Python3.7 pyodbc完美配置訪問access數(shù)據(jù)庫
最近小編需要學(xué)習(xí)python連接access數(shù)據(jù)庫,發(fā)現(xiàn)很多朋友推薦pyodbc,那么這篇文章就先為大家介紹一下Python3.7下pyodbc的配置方法2019-10-10