實例詳解Python裝飾器與閉包
閉包是Python裝飾器的基礎(chǔ)。要理解閉包,先要了解Python中的變量作用域規(guī)則。
變量作用域規(guī)則
首先,在函數(shù)中是能訪問全局變量的:
>>> a = 'global var' >>> def foo(): print(a) >>> foo() global var
然后,在一個嵌套函數(shù)中,內(nèi)層函數(shù)能夠訪問在外層函數(shù)中定義的局部變量:
>>> def foo(): a = 'free var' def bar(): print(a) return bar >>> foo()() free var
閉包
上面的嵌套函數(shù)就是閉包。 閉包 是指延伸了作用域的函數(shù),在其中能夠訪問未在函數(shù)定義體中定義的非全局變量。未在函數(shù)定義體中定義的非全局變量一般都是在嵌套函數(shù)中出現(xiàn)的。
上述示例中的變量a就是一個并未在函數(shù)bar中定義的非全局變量。對于bar來說,它有個專業(yè)名字,叫做 自由變量 。
自由變量的名稱可以在字節(jié)碼對象中查看:
>>> bar = foo() >>> bar.__code__.co_freevars ('a',)
自由變量的值綁定在函數(shù)的__closure__屬性中:
>>> bar.__closure__ (<cell at 0x000001CB2912DF48: str object at 0x000001CB291D3D70>,)
其中保存了對應(yīng)自由變量的cell對象的序列,cell對象的cell_contents屬性保存了變量的值:
>>> bar.__closure__[0].cell_contents 'free var'
這與JavaScript中閉包的行為是類似的,JavaScript中嵌套函數(shù)會將外層函數(shù)的活動對象添加到它的作用域鏈中。但與JavaScript不同的是,當(dāng)Python函數(shù)中的全局變量或者自由變量是不可變對象(數(shù)字、字符串、元組等)時,是只能讀取,無法更新的:
>>> 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
兩種情況下,都會報錯。這并不是缺陷,而是Python的設(shè)計選擇。Python不要求聲明變量,但是會假定在函數(shù)定義體中賦值的變量是局部變量,以避免在不知情的情況下修改全局變量。
a += 1 與 a = a + 1 相同,編譯函數(shù)的定義體時,會將a當(dāng)做局部變量,不會當(dāng)做自由變量保存。然后嘗試獲取a的值時,發(fā)現(xiàn)a并沒有綁定值,于是報錯。
解決這個問題的辦法,一是將變量置于一些可變對象,如列表、字典中:
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)自由變量本身是可變對象時,是可以直接進(jìn)行操作的:
def make_avg(): ls = [] def avg(x): ls.append(x) print(sum(ls)/len(ls)) return avg
裝飾器
裝飾器是可調(diào)用對象,參數(shù)一般是另一個函數(shù)。裝飾器可以以某種方式增強(qiáng)被裝飾函數(shù)的行為,然后返回被裝飾的函數(shù)或者將其替換成一個新的函數(shù)。
一個最簡單的不做任何額外行為的裝飾器:
def decorate(func): return func
decorate 函數(shù)就是一個最簡單的裝飾器,使用方法:
def target(): pass target = decorate(target)
Python為裝飾器的使用提供了語法糖,可以簡便的寫為:
@decorate def target(): pass
導(dǎo)入時運(yùn)行
裝飾器一個很重要的特性是它是導(dǎo)入時(加載模塊時)運(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)入時運(yùn)行的,而被裝飾的函數(shù)是明確調(diào)用時運(yùn)行的。
裝飾器可以返回被裝飾的函數(shù)本身,和運(yùn)行時導(dǎo)入的特性結(jié)合起來,可以實現(xiàn)簡單的注冊器功能:
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ù),但裝飾器的典型行為還是返回一個新函數(shù):把被裝飾的函數(shù)替換成新函數(shù),新函數(shù)接受與原函數(shù)相同的參數(shù),并且返回原函數(shù)本該返回的值。寫法類似于:
def deco(func): def new_func(*args, **kwargs): return func(*args, **kwargs) return new_func
這種情況下裝飾器就使用到了閉包。JavaScript中的防抖與節(jié)流函數(shù)就是這種典型的裝飾器行為。新函數(shù)一般會使用外部裝飾器函數(shù)中的變量當(dāng)做自由變量,對函數(shù)作出某種增強(qiáng)行為。
舉個例子,我們知道,當(dāng)Python函數(shù)的參數(shù)是個可變對象時,會產(chǎn)生意料之外的行為:
def foo(x, y=[]): y.append(x) print(y) foo(1) foo(2) foo(3)
輸出:
[1] [1, 2] [1, 2, 3]
這是因為,函數(shù)的參數(shù)默認(rèn)值保存在__defaults__屬性中,指向了同一個列表:
>>> foo.__defaults__ ([1, 2, 3],)
我們就可以用一個裝飾器在函數(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)建一個裝飾器工廠,接受參數(shù),返回一個裝飾器,再把它應(yīng)用到被裝飾的函數(shù)上,語法如下:
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ù)注冊到某些中央注冊處。之前我們曾經(jīng)實現(xiàn)過一個簡單的注冊裝飾器,只是注冊了view函數(shù),卻沒有URL映射,是遠(yuǎn)遠(yuǎn)不夠的。
在Flask中,注冊view函數(shù)需要一個裝飾器:
@app.route('/hello') def hello(): return 'Hello, World'
原理就是使用了裝飾器工廠,可以簡單的模擬一下實現(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
還可以使用裝飾器工廠來確定view函數(shù)可以允許哪些HTTP請求方法:
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))
類裝飾器
裝飾器的參數(shù)也可以是一個類,也就是說,裝飾器可以裝飾類:
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é)
以上所述是小編給大家介紹的實例詳解Python裝飾器與閉包,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復(fù)大家的。在此也非常感謝大家對腳本之家網(wǎng)站的支持!
如果你覺得本文對你有幫助,歡迎轉(zhuǎn)載,煩請注明出處,謝謝!
- Python 中的函數(shù)裝飾器和閉包詳解
- Python閉包裝飾器使用方法匯總
- Python閉包及裝飾器運(yùn)行原理解析
- Python閉包與裝飾器原理及實例解析
- Python高級特性之閉包與裝飾器實例詳解
- 詳解python中的生成器、迭代器、閉包、裝飾器
- Python閉包和裝飾器用法實例詳解
- 詳解 Python中LEGB和閉包及裝飾器
- python中函數(shù)總結(jié)之裝飾器閉包詳解
- 深入理解python中的閉包和裝飾器
- Python的幾個高級語法概念淺析(lambda表達(dá)式閉包裝飾器)
- 簡析Python的閉包和裝飾器
- 詳解Python中的裝飾器、閉包和functools的教程
- python高級語法之閉包和裝飾器詳解
相關(guān)文章
python 實現(xiàn)的發(fā)送郵件模板【普通郵件、帶附件、帶圖片郵件】
這篇文章主要介紹了python 實現(xiàn)的發(fā)送郵件模板,包含Python發(fā)送普通郵件、帶附件及帶圖片郵件相關(guān)實現(xiàn)技巧,需要的朋友可以參考下2019-07-07Python入門之實例方法、類方法和靜態(tài)方法的區(qū)別講解
這篇文章主要介紹了Python入門之實例方法、類方法和靜態(tài)方法的區(qū)別講解,實例方法是在創(chuàng)建了類的實例之后才能被調(diào)用的方法,類方法是在不需要創(chuàng)建類的實例的情況下就可以調(diào)用的方法,最后,靜態(tài)方法是與類和類的實例都沒有綁定關(guān)系的方法,需要的朋友可以參考下2023-10-10Pycharm中使用git進(jìn)行合作開發(fā)的教程詳解
這篇文章主要介紹了Pycharm中使用git進(jìn)行合作開發(fā),本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-11-11python實現(xiàn)銀行實戰(zhàn)系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了python實現(xiàn)銀行實戰(zhàn)系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2020-02-02selenium設(shè)置proxy、headers的方法(phantomjs、Chrome、Firefox)
這篇文章主要介紹了selenium設(shè)置proxy、headers的方法(phantomjs、Chrome、Firefox),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-11-11