實(shí)例詳解Python裝飾器與閉包
閉包是Python裝飾器的基礎(chǔ)。要理解閉包,先要了解Python中的變量作用域規(guī)則。
變量作用域規(guī)則
首先,在函數(shù)中是能訪問(wèn)全局變量的:
>>> a = 'global var' >>> def foo(): print(a) >>> foo() global var
然后,在一個(gè)嵌套函數(shù)中,內(nèi)層函數(shù)能夠訪問(wèn)在外層函數(shù)中定義的局部變量:
>>> def foo(): a = 'free var' def bar(): print(a) return bar >>> foo()() free var
閉包
上面的嵌套函數(shù)就是閉包。 閉包 是指延伸了作用域的函數(shù),在其中能夠訪問(wèn)未在函數(shù)定義體中定義的非全局變量。未在函數(shù)定義體中定義的非全局變量一般都是在嵌套函數(shù)中出現(xiàn)的。
上述示例中的變量a就是一個(gè)并未在函數(shù)bar中定義的非全局變量。對(duì)于bar來(lái)說(shuō),它有個(gè)專(zhuān)業(yè)名字,叫做 自由變量 。
自由變量的名稱(chēng)可以在字節(jié)碼對(duì)象中查看:
>>> bar = foo()
>>> bar.__code__.co_freevars
('a',)
自由變量的值綁定在函數(shù)的__closure__屬性中:
>>> bar.__closure__ (<cell at 0x000001CB2912DF48: str object at 0x000001CB291D3D70>,)
其中保存了對(duì)應(yīng)自由變量的cell對(duì)象的序列,cell對(duì)象的cell_contents屬性保存了變量的值:
>>> bar.__closure__[0].cell_contents 'free var'
這與JavaScript中閉包的行為是類(lèi)似的,JavaScript中嵌套函數(shù)會(huì)將外層函數(shù)的活動(dòng)對(duì)象添加到它的作用域鏈中。但與JavaScript不同的是,當(dāng)Python函數(shù)中的全局變量或者自由變量是不可變對(duì)象(數(shù)字、字符串、元組等)時(shí),是只能讀取,無(wú)法更新的:
>>> a = 1 >>> def foo(): print(a) a += 1 >>> foo() UnboundLocalError: local variable 'a' referenced before assignment >>> def foo(): a = 1 def bar(): print(a) a += 1 return bar >>> foo()() UnboundLocalError: local variable 'a' referenced before assignment
兩種情況下,都會(huì)報(bào)錯(cuò)。這并不是缺陷,而是Python的設(shè)計(jì)選擇。Python不要求聲明變量,但是會(huì)假定在函數(shù)定義體中賦值的變量是局部變量,以避免在不知情的情況下修改全局變量。
a += 1 與 a = a + 1 相同,編譯函數(shù)的定義體時(shí),會(huì)將a當(dāng)做局部變量,不會(huì)當(dāng)做自由變量保存。然后嘗試獲取a的值時(shí),發(fā)現(xiàn)a并沒(méi)有綁定值,于是報(bào)錯(cuò)。
解決這個(gè)問(wèn)題的辦法,一是將變量置于一些可變對(duì)象,如列表、字典中:
def foo():
ns = {}
ns['a'] = 1
def bar():
ns['a'] += 1
print (ns['a'])
return bar
另外的方法就是使用 global 或者 nonlocal 將變量聲明為全局變量或者自由變量:
>>> def foo(): a = 1 def bar(): nonlocal a a += 1 print(a) return bar >>> foo()() 2
當(dāng)自由變量本身是可變對(duì)象時(shí),是可以直接進(jìn)行操作的:
def make_avg(): ls = [] def avg(x): ls.append(x) print(sum(ls)/len(ls)) return avg
裝飾器
裝飾器是可調(diào)用對(duì)象,參數(shù)一般是另一個(gè)函數(shù)。裝飾器可以以某種方式增強(qiáng)被裝飾函數(shù)的行為,然后返回被裝飾的函數(shù)或者將其替換成一個(gè)新的函數(shù)。
一個(gè)最簡(jiǎn)單的不做任何額外行為的裝飾器:
def decorate(func): return func
decorate 函數(shù)就是一個(gè)最簡(jiǎn)單的裝飾器,使用方法:
def target(): pass target = decorate(target)
Python為裝飾器的使用提供了語(yǔ)法糖,可以簡(jiǎn)便的寫(xiě)為:
@decorate def target(): pass
導(dǎo)入時(shí)運(yùn)行
裝飾器一個(gè)很重要的特性是它是導(dǎo)入時(shí)(加載模塊時(shí))運(yùn)行的:
def decorate(func):
print('running decorator when import')
return func
@decorate
def foo():
print('running foo')
pass
if __name__ == '__main__':
print('start foo')
foo()
結(jié)果:
running decorator when import start foo running foo
可以看到,裝飾器是導(dǎo)入時(shí)運(yùn)行的,而被裝飾的函數(shù)是明確調(diào)用時(shí)運(yùn)行的。
裝飾器可以返回被裝飾的函數(shù)本身,和運(yùn)行時(shí)導(dǎo)入的特性結(jié)合起來(lái),可以實(shí)現(xiàn)簡(jiǎn)單的注冊(cè)器功能:
view_registry = [] def register(func): view_registry.append(func) return func @register def view1(): pass @register def view2(): pass def main(): print(view_registry) if __name__ == '__main__': main()
返回新函數(shù)
上述裝飾器的例子都返回了被裝飾的原函數(shù),但裝飾器的典型行為還是返回一個(gè)新函數(shù):把被裝飾的函數(shù)替換成新函數(shù),新函數(shù)接受與原函數(shù)相同的參數(shù),并且返回原函數(shù)本該返回的值。寫(xiě)法類(lèi)似于:
def deco(func): def new_func(*args, **kwargs): return func(*args, **kwargs) return new_func
這種情況下裝飾器就使用到了閉包。JavaScript中的防抖與節(jié)流函數(shù)就是這種典型的裝飾器行為。新函數(shù)一般會(huì)使用外部裝飾器函數(shù)中的變量當(dāng)做自由變量,對(duì)函數(shù)作出某種增強(qiáng)行為。
舉個(gè)例子,我們知道,當(dāng)Python函數(shù)的參數(shù)是個(gè)可變對(duì)象時(shí),會(huì)產(chǎn)生意料之外的行為:
def foo(x, y=[]): y.append(x) print(y) foo(1) foo(2) foo(3)
輸出:
[1] [1, 2] [1, 2, 3]
這是因?yàn)?,函?shù)的參數(shù)默認(rèn)值保存在__defaults__屬性中,指向了同一個(gè)列表:
>>> foo.__defaults__ ([1, 2, 3],)
我們就可以用一個(gè)裝飾器在函數(shù)執(zhí)行前取出默認(rèn)值做深復(fù)制,然后覆蓋函數(shù)原先的參數(shù)默認(rèn)值:
import copy
def fresh_defaults(func):
defaults = func.__defaults__
def deco(*args, **kwargs):
func.__defaults__ = copy.deepcopy(defaults)
return func(*args, **kwargs)
return deco
@fresh_defaults
def foo(x, y=[]):
y.append(x)
print(y)
foo(1)
foo(2)
foo(3)
輸出:
[1] [2] [3]
接收參數(shù)的裝飾器
裝飾器除了可以接受函數(shù)作為參數(shù)外,還可以接受其他參數(shù)。使用方法是:創(chuàng)建一個(gè)裝飾器工廠,接受參數(shù),返回一個(gè)裝飾器,再把它應(yīng)用到被裝飾的函數(shù)上,語(yǔ)法如下:
def deco_factory(*args, **kwargs):
def deco(func):
print(args)
return func
return deco
@deco_factory('factory')
def foo():
pass
在Web框架中,通常要將URL模式映射到生成響應(yīng)的view函數(shù),并將view函數(shù)注冊(cè)到某些中央注冊(cè)處。之前我們?cè)?jīng)實(shí)現(xiàn)過(guò)一個(gè)簡(jiǎn)單的注冊(cè)裝飾器,只是注冊(cè)了view函數(shù),卻沒(méi)有URL映射,是遠(yuǎn)遠(yuǎn)不夠的。
在Flask中,注冊(cè)view函數(shù)需要一個(gè)裝飾器:
@app.route('/hello')
def hello():
return 'Hello, World'
原理就是使用了裝飾器工廠,可以簡(jiǎn)單的模擬一下實(shí)現(xiàn):
class App:
def __init__(self):
self.view_functions = {}
def route(self, rule):
def deco(view_func):
self.view_functions[rule] = view_func
return view_func
return deco
app = App()
@app.route('/')
def index():
pass
@app.route('/hello')
def hello():
pass
for rule, view in app.view_functions.items():
print(rule, ':', view.__name__)
輸出:
/ : index /hello : hello
還可以使用裝飾器工廠來(lái)確定view函數(shù)可以允許哪些HTTP請(qǐng)求方法:
def action(methods):
def deco(view):
view.allow_methods = [method.lower() for method in methods]
return view
return deco
@action(['GET', 'POST'])
def view(request):
if request.method.lower() in view.allow_methods:
...
重疊的裝飾器
裝飾器也是可以重疊使用的:
@d1 @d2 def foo(): pass
等同于:
foo = d1(d2(foo))
類(lèi)裝飾器
裝飾器的參數(shù)也可以是一個(gè)類(lèi),也就是說(shuō),裝飾器可以裝飾類(lèi):
import types
def deco(cls):
for key, method in cls.__dict__.items():
if isinstance(method, types.FunctionType):
print(key, ':', method.__name__)
return cls
@deco
class Test:
def __init__(self):
pass
def foo(self):
pass
總結(jié)
以上所述是小編給大家介紹的實(shí)例詳解Python裝飾器與閉包,希望對(duì)大家有所幫助,如果大家有任何疑問(wèn)請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!
如果你覺(jué)得本文對(duì)你有幫助,歡迎轉(zhuǎn)載,煩請(qǐng)注明出處,謝謝!
相關(guān)文章
Python中json.dumps()和json.dump()的區(qū)別小結(jié)
在Python中,json.dumps()和json.dump()是兩個(gè)常用的函數(shù),本文主要介紹了Python中json.dumps()和json.dump()的區(qū)別小結(jié),具有一定的參考價(jià)值,感興趣的可以了解一下2024-02-02
Python數(shù)值求解微分方程方法(歐拉法,隱式歐拉)
這篇文章主要介紹了Python數(shù)值求解微分方程方法(歐拉法,隱式歐拉),文章圍繞主題展開(kāi)詳細(xì)的內(nèi)介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-09-09
python中SSH遠(yuǎn)程登錄設(shè)備的實(shí)現(xiàn)方法
本文主要介紹了python中SSH遠(yuǎn)程登錄設(shè)備,python中支持SSH協(xié)議的模塊主要有Paramiko和netmiko兩種,本文主要介紹了netmiko模塊,具有一定的參考價(jià)值,感興趣的可以了解一下2022-04-04
python條件變量之生產(chǎn)者與消費(fèi)者操作實(shí)例分析
這篇文章主要介紹了python條件變量之生產(chǎn)者與消費(fèi)者操作,結(jié)合具體實(shí)例形式分析了Python條件變量的概念、原理、及線程操作的相關(guān)技巧,需要的朋友可以參考下2017-03-03
python畫(huà)一個(gè)圣誕樹(shù)實(shí)現(xiàn)示例
這篇文章主要為大家介紹了使用python畫(huà)一個(gè)圣誕樹(shù)實(shí)現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05
神經(jīng)網(wǎng)絡(luò)訓(xùn)練采用gpu設(shè)置的方式
這篇文章主要介紹了神經(jīng)網(wǎng)絡(luò)訓(xùn)練采用gpu設(shè)置的方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-03-03
Python使用嵌套循環(huán)實(shí)現(xiàn)圖像處理算法
這篇文章主要給大家詳細(xì)介紹Python如何使用嵌套循環(huán)實(shí)現(xiàn)圖像處理算法,文中有詳細(xì)的代碼示例,具有一定的參考價(jià)值,需要的朋友可以參考下2023-07-07
構(gòu)建Python中的分布式系統(tǒng)結(jié)合Celery與RabbitMQ
在本文中,我們深入探討了如何利用Celery和RabbitMQ構(gòu)建Python中的分布式系統(tǒng),我們首先介紹了Celery和RabbitMQ的概念及其優(yōu)勢(shì),然后展示了如何結(jié)合它們來(lái)創(chuàng)建一個(gè)簡(jiǎn)單但功能強(qiáng)大的分布式系統(tǒng),感興趣的朋友跟隨小編一起看看吧2024-05-05
Python編程中使用Pillow來(lái)處理圖像的基礎(chǔ)教程
這篇文章主要介紹了Python編程中使用Pillow來(lái)處理圖像的基礎(chǔ)教程,Pillow和PIL都是Python下十分強(qiáng)大的圖片處理利器,朋友可以參考下2015-11-11

