python装饰器的通俗理解_简单理解Python装饰器
Python有大量強(qiáng)大又貼心的特性,如果要列個(gè)最受歡迎排行榜,那么裝飾器絕對(duì)會(huì)在其中。
剛接觸裝飾器,會(huì)覺得代碼不多卻難以理解。其實(shí)裝飾器的語法本身挺簡單的,復(fù)雜是因?yàn)橥瑫r(shí)混雜了其它的概念。下面我們一起拋去無關(guān)概念,簡單地理解下Python的裝飾器。
裝飾器的原理
在解釋器下跑個(gè)裝飾器的例子,直觀地感受一下
# make_bold就是裝飾器,實(shí)現(xiàn)方式這里略去
>>> @make_bold
... def get_content():
... return "hello world"
...
>>> get_content()
"hello world"
被make_bold裝飾的get_content,調(diào)用后返回結(jié)果會(huì)自動(dòng)被b標(biāo)簽包住。怎么做到的呢,簡單4步就能明白了。
1. 函數(shù)是對(duì)象
我們定義個(gè)get_content函數(shù)。這時(shí)get_content也是個(gè)對(duì)象,它能做所有對(duì)象的操作。
它有id,有type,有值。
>>> id(get_content)
140090200473112
>>> type(get_content)
>>> get_content
跟其他對(duì)象一樣可以被賦值給其它變量。
>>> 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ù)對(duì)象
我們可以用class來構(gòu)造函數(shù)對(duì)象。有成員函數(shù)__call__的就是函數(shù)對(duì)象了,函數(shù)對(duì)象被調(diào)用時(shí)正是調(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ù)對(duì)象的使用分兩步:構(gòu)造和調(diào)用(同學(xué)們注意了,這是考點(diǎn))。
>>> fo = FuncObj("python")
Initialize
>>> fo()
"Hi python"
3. @是個(gè)語法糖
裝飾器的@沒有做什么特別的事,不用它也可以實(shí)現(xiàn)一樣的功能,只不過需要更多的代碼。
@make_bold
def get_content():
return "hello world"
上面的代碼等價(jià)于下面的
def get_content():
return "hello world"
get_content = make_bold(get_content)
make_bold是個(gè)函數(shù),要求入?yún)⑹呛瘮?shù)對(duì)象,返回值是函數(shù)對(duì)象。@的語法糖其實(shí)是省去了上面最后一行代碼,使可讀性更好。用了裝飾器后,每次調(diào)用get_content,真正調(diào)用的是make_bold返回的函數(shù)對(duì)象。
4. 用類實(shí)現(xiàn)裝飾器
入?yún)⑹呛瘮?shù)對(duì)象,返回是函數(shù)對(duì)象,如果第2步里的類的構(gòu)造函數(shù)改成入?yún)⑹莻€(gè)函數(shù)對(duì)象,不就正好符合要求嗎?我們來試試實(shí)現(xiàn)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"
成功實(shí)現(xiàn)裝飾器!是不是很簡單?
這里分析一下之前強(qiáng)調(diào)的構(gòu)造和調(diào)用兩個(gè)過程。我們?nèi)サ?#64;語法糖好理解一些。
# 構(gòu)造,使用裝飾器時(shí)構(gòu)造函數(shù)對(duì)象,調(diào)用了__init__
>>> get_content = make_bold(get_content)
Initialize
# 調(diào)用,實(shí)際上直接調(diào)用的是make_bold構(gòu)造出來的函數(shù)對(duì)象
>>> get_content()
Call
"hello world"
到這里就徹底清楚了,完結(jié)撒花,可以關(guān)掉網(wǎng)頁了~~~(如果只是想知道裝飾器原理的話)
函數(shù)版裝飾器
閱讀源碼時(shí),經(jīng)常見到用嵌套函數(shù)實(shí)現(xiàn)的裝飾器,怎么理解?同樣僅需4步。
1. def的函數(shù)對(duì)象初始化
用class實(shí)現(xiàn)的函數(shù)對(duì)象很容易看到什么時(shí)候構(gòu)造的,那def定義的函數(shù)對(duì)象什么時(shí)候構(gòu)造的呢?
# 這里的全局變量刪去了無關(guān)的內(nèi)容
>>> globals()
{}
>>> def func():
... pass
...
>>> globals()
{"func": }
不像一些編譯型語言,程序在啟動(dòng)時(shí)函數(shù)已經(jīng)構(gòu)造那好了。上面的例子可以看到,執(zhí)行到def會(huì)才構(gòu)造出一個(gè)函數(shù)對(duì)象,并賦值給變量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í)函數(shù)對(duì)象才創(chuàng)建,因此每次調(diào)用outer都會(huì)創(chuàng)建一個(gè)新的inner。
#下面可以看出,每次返回的inner是不同的。
>>> outer()
Before def: {}
After def: {"inner": .inner at 0x7f0b18fa0048>}
.inner at 0x7f0b18fa0048>
>>> outer()
Before def: {}
After def: {"inner": .inner at 0x7f0b18fa00d0>}
.inner at 0x7f0b18fa00d0>
3. 閉包
嵌套函數(shù)有什么特別之處?因?yàn)橛虚]包。
def outer():
msg = "hello world"
def inner():
print(msg)
return inner
下面的試驗(yàn)表明,inner可以訪問到outer的局部變量msg。
>>> func = outer()
>>> func()
hello world
閉包有2個(gè)特點(diǎn)
inner能訪問outer及其祖先函數(shù)的命名空間內(nèi)的變量(局部變量,函數(shù)參數(shù))。
調(diào)用outer已經(jīng)返回了,但是它的命名空間被返回的inner對(duì)象引用,所以還不會(huì)被回收。
這部分想深入可以去了解Python的LEGB規(guī)則。
4. 用函數(shù)實(shí)現(xiàn)裝飾器
裝飾器要求入?yún)⑹呛瘮?shù)對(duì)象,返回值是函數(shù)對(duì)象,嵌套函數(shù)完全能勝任。
def make_bold(func):
print("Initialize")
def wrapper():
print("Call")
return "{}".format(func())
return wrapper
用法跟類實(shí)現(xiàn)的裝飾器一樣。可以去掉@語法糖分析下構(gòu)造和調(diào)用的時(shí)機(jī)。
>>> @make_bold
... def get_content():
... return "hello world"
...
Initialize
>>> get_content()
Call
"hello world"
因?yàn)榉祷氐膚rapper還在引用著,所以存在于make_bold命名空間的func不會(huì)消失。make_bold可以裝飾多個(gè)函數(shù),wrapper不會(huì)調(diào)用混淆,因?yàn)槊看握{(diào)用make_bold,都會(huì)有創(chuàng)建新的命名空間和新的wrapper。
到此函數(shù)實(shí)現(xiàn)裝飾器也理清楚了,完結(jié)撒花,可以關(guān)掉網(wǎng)頁了~~~(后面是使用裝飾的常見問題)
常見問題
1. 怎么實(shí)現(xiàn)帶參數(shù)的裝飾器?
帶參數(shù)的裝飾器,有時(shí)會(huì)異常的好用。我們看個(gè)例子。
>>> @make_header(2)
... def get_content():
... return "hello world"
...
>>> get_content()
"
hello world
"#怎么做到的呢?其實(shí)這跟裝飾器語法沒什么關(guān)系。去掉@語法糖會(huì)變得很容易理解。
@make_header(2)
def get_content():
return "hello world"
# 等價(jià)于
def get_content():
return "hello world"
unnamed_decorator = make_header(2)
get_content = unnamed_decorator(get_content)
上面代碼中的unnamed_decorator才是真正的裝飾器,make_header是個(gè)普通的函數(shù),它的返回值是裝飾器。
來看一下實(shí)現(xiàn)的代碼。
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
看了實(shí)現(xiàn)代碼,裝飾器的構(gòu)造和調(diào)用的時(shí)序已經(jīng)很清楚了。
>>> @make_header(2)
... def get_content():
... return "hello world"
...
Create decorator
Initialize
>>> get_content()
Call
"
hello world
"2. 如何裝飾有參數(shù)的函數(shù)?
為了有條理地理解裝飾器,之前例子里的被裝飾函數(shù)有意設(shè)計(jì)成無參的。我們來看個(gè)例子。
@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 "{}".format(self.func(name))
如果被裝飾的函數(shù)參數(shù)是明確固定的,這么寫是沒有問題的。但是make_bold明顯不是這種場景。它既需要裝飾沒有參數(shù)的get_content,又需要裝飾有參數(shù)的get_login_tip。這時(shí)候就需要可變參數(shù)了。
class make_bold(object):
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
return "{}".format(self.func(*args, **kwargs))
當(dāng)裝飾器不關(guān)心被裝飾函數(shù)的參數(shù),或是被裝飾函數(shù)的參數(shù)多種多樣的時(shí)候,可變參數(shù)非常合適。可變參數(shù)不屬于裝飾器的語法內(nèi)容,這里就不深入探討了。
3. 一個(gè)函數(shù)能否被多個(gè)裝飾器裝飾?
下面這么寫合法嗎?
@make_italic
@make_bold
def get_content():
return "hello world"
合法。上面的的代碼和下面等價(jià),留意一下裝飾的順序。
def get_content():
return "hello world"
get_content = make_bold(get_content) # 先裝飾離函數(shù)定義近的
get_content = make_italic(get_content)
4. functools.wraps有什么用?
Python的裝飾器倍感貼心的地方是對(duì)調(diào)用方透明。調(diào)用方完全不知道也不需要知道調(diào)用的函數(shù)被裝飾了。這樣我們就能在調(diào)用方的代碼完全不改動(dòng)的前提下,給函數(shù)patch功能。
為了對(duì)調(diào)用方透明,裝飾器返回的對(duì)象要偽裝成被裝飾的函數(shù)。偽裝得越像,對(duì)調(diào)用方來說差異越小。有時(shí)光偽裝函數(shù)名和參數(shù)是不夠的,因?yàn)镻ython的函數(shù)對(duì)象有一些元信息調(diào)用方可能讀取了。為了連這些元信息也偽裝上,functools.wraps出場了。它能用于把被調(diào)用函數(shù)的__module__,__name__,__qualname__,__doc__,__annotations__賦值給裝飾器返回的函數(shù)對(duì)象。
import functools
def make_bold(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return "{}".format(func(*args, **kwargs))
return wrapper
對(duì)比一下效果。
>>> @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)裝飾器時(shí)往往不知道調(diào)用方會(huì)怎么用,所以養(yǎng)成好習(xí)慣加上functools.wraps吧。
總結(jié)
以上是生活随笔為你收集整理的python装饰器的通俗理解_简单理解Python装饰器的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Win11有黑色边框怎么办 Win11有
- 下一篇: 贪吃蛇的编程python_python实