Python的裝飾器是一大利器,幾乎所有的流行框架,都在廣泛使用裝飾器
為什么說沒有閉包( closure) ,裝飾器的功能會大打折扣?
一個(gè)最簡易的裝飾器是什么都不不做,只是透傳一個(gè)函數(shù)對象
如
def final(f):
return f
裝飾器的原理是
@deco
def func():
....
func() # call
等價(jià)于
def func():
...
func = deco(func) # 注意這里
func()
func = deco(func) 右邊的表達(dá)式有可能偷梁換柱把函數(shù)換成了另外一個(gè)函數(shù),有可能沒有換,只是在對func對象做了一些事情,比如加一點(diǎn)屬性之類的,也可以只是在func調(diào)用之前增加一些別的事情。
deco(func) 內(nèi)部做什么,靈活性極大
這也是裝飾器的特點(diǎn)之一——不需要修改func的定義和實(shí)現(xiàn),既可以靈活地操縱它的調(diào)用行為,借用動態(tài)語言的特性,既可以動態(tài)注入屬性,也可以添加想要的邏輯,很靈活。
更多的一種裝飾器是——偷梁換柱。
即經(jīng)過修飾之后函數(shù)已經(jīng)不是原來那個(gè)??赡茉谘b飾器器內(nèi)部已經(jīng)計(jì)算出結(jié)果,同時(shí)轉(zhuǎn)換成另外一個(gè)函數(shù)。
這個(gè)“另外”的函數(shù)通常是通過閉包來提供的。
def deco(func):
def wrapper():
ret = func()
return ret
return wrapper
閉包在python中有一個(gè) __closure__ 協(xié)議
代碼
def advance_avg():
ls = [] # 只初始化一次
s = 1
def wrapper(num):
nonlocal s # 必須要聲明 nolocal 否則會報(bào)告未知屬性錯(cuò)誤
s += num
print(f"s is {s}")
ls.append(num)
total = sum(ls)
return total / len(ls)
return wrapper
當(dāng)調(diào)用一次 advance_avg() 的時(shí)候,s和ls都會綁定在對象上
adv = advance_avg() # int 和 list對象都已經(jīng)綁定
以后每次調(diào)用adv(val) 綁定的ls和s都可以被內(nèi)部的函數(shù)wrapper操作。而且一大特性是這兩個(gè)變量可以緩存上一次調(diào)用之后計(jì)算的結(jié)果,且看
adv = advance_avg()
adv(1) # 結(jié)果是1
adv(2) # 結(jié)果是1.5 不是2
adv.__closure__ # 輸出 <cell at 0x7f8da8143520: list object at 0x7f8d9801db40>, <cell at 0x7f8da8143910: int object at 0x108f92ae0>
adv的 __closure__ 屬性輸出了一個(gè)int 和 list對象地址,這兩個(gè)對象的生命周期跟隨adv 。
理解閉包關(guān)鍵的地方在于作用域的概念。
有個(gè)可能不太精確的類比
對于嵌套閉包,我們可以把最上層的函數(shù)定義視為一個(gè)類定義,在python 中一切皆對象,包括函數(shù)
對于類的寫法,我們同樣可以寫一個(gè)上述用起來一模一樣的功能類,只是需要在類內(nèi)部定義一下__call__ —— 大佬推薦的工程級別的裝飾器寫法,用定義了__call__ 函數(shù)的類
class Average:
def __init__(self):
self.series = []
self.s = 0
def __call__(self, new_value):
self.series.append(new_value)
total = sum(self.series)
return total / len(self.series)
這個(gè)類用起來跟上面的閉包函數(shù)形式上沒有太大的區(qū)別。
__init__ 函數(shù)有一次性聲明并且追隨Average對象聲明周期的兩個(gè)變量,這個(gè)和閉包一樣,帶閉包的 advance_avg函數(shù)對象保持著一個(gè)int和一個(gè)list的生命周期
總結(jié)
- 類的方法,可以訪問類內(nèi)的self屬性
- 方法中的方法(閉包)可以訪問上層方法定義的局部屬性(類比與類內(nèi)定義的self 方法)
- 函數(shù)可以多層嵌套,訪問范圍是內(nèi)層有外層的訪問權(quán),但是反過來不行——否則封裝毫無意義
- 閉包訪問上層局部變量,如果變量是不可變對象,一定要使用 nolocal聲明,否則會報(bào)告未知屬性錯(cuò)誤