python 裝飾器岂嗓、閉包詳解

一直不理解裝飾器原理互站,在知乎上找到一篇詳細(xì)的講解文章私蕾,原文貼了上來僵缺。
知乎裝飾器原文鏈接

1. Python中一切皆對象

這恐怕是學(xué)習(xí)Python最有用的一句話胡桃。想必你已經(jīng)知道Python中的list, tuple, dict等內(nèi)置數(shù)據(jù)結(jié)構(gòu),當(dāng)你執(zhí)行:
alist = [1, 2, 3]
時(shí)磕潮,你就創(chuàng)建了一個(gè)列表對象翠胰,并且用alist這個(gè)變量引用它:

image

當(dāng)然你也可以自己定義一個(gè)類:

class House(object):
    def __init__(self, area, city):
        self.area = area
        self.city = city

    def sell(self, price):
        [...]  #other code
        return price

然后創(chuàng)建一個(gè)類的對象:
house = House(200, 'Shanghai')

OK,你立馬就在上海有了一套200平米的房子自脯,它有一些屬性(area, city)之景,和一些方法(init, self):

image

2. 函數(shù)是第一類對象

和list, tuple, dict以及用House創(chuàng)建的對象一樣,當(dāng)你定義一個(gè)函數(shù)時(shí)膏潮,函數(shù)也是對象:

def func(a, b):
    return a+b
image

在全局域锻狗,函數(shù)對象被函數(shù)名引用著,它接收兩個(gè)參數(shù)a和b焕参,計(jì)算這兩個(gè)參數(shù)的和作為返回值轻纪。

所謂第一類對象,意思是可以用標(biāo)識符給對象命名叠纷,并且對象可以被當(dāng)作數(shù)據(jù)處理刻帚,例如賦值、作為參數(shù)傳遞給函數(shù)涩嚣,或者作為返回值return 等

因此崇众,你完全可以用其他變量名引用這個(gè)函數(shù)對象:
add = func

image

這樣,你就可以像調(diào)用func(1, 2)一樣航厚,通過新的引用調(diào)用函數(shù)了:

print func(1, 2)
print add(1, 2)  #the same as func(1, 2)

或者將函數(shù)對象作為參數(shù)顷歌,傳遞給另一個(gè)函數(shù):

def caller_func(f):
    return f(1, 2)

if __name__ == "__main__":
    print caller_func(func)
image

可以看到,

  • 函數(shù)對象func作為參數(shù)傳遞給caller_func函數(shù)幔睬,傳參過程類似于一個(gè)賦值操作f=func眯漩;
  • 于是func函數(shù)對象,被caller_func函數(shù)作用域中的局部變量f引用溪窒,f實(shí)際指向了函數(shù)func坤塞;cc
  • 當(dāng)執(zhí)行return f(1, 2)的時(shí)候,相當(dāng)于執(zhí)行了return func(1, 2)澈蚌;

因此輸出結(jié)果為3摹芙。

3. 函數(shù)對象 vs 函數(shù)調(diào)用

無論是把函數(shù)賦值給新的標(biāo)識符,還是作為參數(shù)傳遞給新的函數(shù)宛瞄,針對的都是函數(shù)對象本身浮禾,而不是函數(shù)的調(diào)用交胚。

用一個(gè)更加簡單,但從外觀上看盈电,更容易產(chǎn)生混淆的例子來說明這個(gè)問題蝴簇。例如定義了下面這個(gè)函數(shù):

def func():
    return "hello,world"

然后分別執(zhí)行兩次賦值:

ref1 = func      #將函數(shù)對象賦值給ref1
ref2 = func()       #調(diào)用函數(shù),將函數(shù)的返回值("hello,world"字符串)賦值給ref2

很多初學(xué)者會混淆這兩種賦值匆帚,通過Python內(nèi)建的type函數(shù)熬词,可以查看一下這兩次賦值的結(jié)果:

In [4]: type(ref1)
Out[4]: function

In [5]: type(ref2)
Out[5]: str

可以看到,ref1引用了函數(shù)對象本身吸重,而ref2則引用了函數(shù)的返回值互拾。通過內(nèi)建的callable函數(shù),可以進(jìn)一步驗(yàn)證ref1是可調(diào)用的嚎幸,而ref2是不可調(diào)用的:

In [9]: callable(ref1)
Out[9]: True

In [10]: callable(ref2)
Out[10]: False

傳參的效果與之類似颜矿。

4. 閉包&LEGB法則

所謂閉包,就是將組成函數(shù)的語句和這些語句的執(zhí)行環(huán)境打包在一起時(shí)嫉晶,得到的對象

聽上去的確有些復(fù)雜骑疆,還是用一個(gè)栗子來幫助理解一下。假設(shè)我們在foo.py模塊中做了如下定義:

#foo.py
filename = "foo.py"

def call_func(f):
    return f()    #如前面介紹的替废,f引用一個(gè)函數(shù)對象箍铭,然后調(diào)用它

在另一個(gè)func.py模塊中,寫下了這樣的代碼:

#func.py
import foo      #導(dǎo)入foo.py

filename = "func.py"
def show_filename():
    return "filename: %s" % filename

if __name__ == "__main__":
    print foo.call_func(show_filename)  #注意:實(shí)際發(fā)生調(diào)用的位置舶担,是在foo.call_func函數(shù)中

當(dāng)我們用python func.py命令執(zhí)行func.py時(shí)輸出結(jié)果為:

chiyu@chiyu-PC:~$ python func.py 
filename:func.py

很顯然show_filename()函數(shù)使用的filename變量的值坡疼,是在與它相同環(huán)境(func.py模塊)中定義的那個(gè)。盡管foo.py模塊中也定義了同名的filename變量衣陶,而且實(shí)際調(diào)用show_filename的位置也是在foo.py的call_func內(nèi)部柄瑰。

而對于嵌套函數(shù),這一機(jī)制則會表現(xiàn)的更加明顯:閉包將會捕捉內(nèi)層函數(shù)執(zhí)行所需的整個(gè)環(huán)境

#enclosed.py
import foo
def wrapper():
    filename = "enclosed.py"
    def show_filename():
        return "filename: %s" % filename
    print foo.call_func(show_filename)    #輸出:filename: enclosed.py

實(shí)際上剪况,每一個(gè)函數(shù)對象教沾,都有一個(gè)指向了該函數(shù)定義時(shí)所在全局名稱空間的globals屬性:

#show_filename inside wrapper
#show_filename.__globals__

{
'__builtins__': <module '__builtin__' (built-in)>,        #內(nèi)建作用域環(huán)境
'__file__': 'enclosed.py',         
'wrapper': <function wrapper at 0x7f84768b6578>,      #直接外圍環(huán)境
'__package__': None,            
'__name__': '__main__',        
'foo': <module 'foo' from '/home/chiyu/foo.pyc'>,         #全局環(huán)境
'__doc__': None                   
}

當(dāng)代碼執(zhí)行到show_filename中的return "filename: %s" % filename語句時(shí),解析器按照下面的順序查找filename變量:

  • Local - 本地函數(shù)(show_filename)內(nèi)部译断,通過任何方式賦值的授翻,而且沒有被global關(guān)鍵字聲明為全局變量的filename變量;
  • Enclosing - 直接外圍空間(上層函數(shù)wrapper)的本地作用域孙咪,查找filename變量(如果有多層嵌套堪唐,則由內(nèi)而外逐層查找,直至最外層的函數(shù))翎蹈;
  • Global - 全局空間(模塊enclosed.py)淮菠,在模塊頂層賦值的filename變量;
  • Builtin - 內(nèi)置模塊(_builtin_)中預(yù)定義的變量名中查找filename變量荤堪;

在任何一層先找到了符合要求的filename變量合陵,則不再向更外層查找枢赔。如果直到Builtin層仍然沒有找到符合要求的變量,則拋出NameError異常拥知。這就是變量名解析的:LEGB法則踏拜。

總結(jié):
閉包最重要的使用價(jià)值在于:封存函數(shù)執(zhí)行的上下文環(huán)境;
閉包在其捕捉的執(zhí)行環(huán)境(def語句塊所在上下文)中低剔,也遵循LEGB規(guī)則逐層查找速梗,直至找到符合要求的變量,或者拋出異常户侥。

5. 裝飾器&語法糖(syntax sugar)

那么閉包和裝飾器又有什么關(guān)系呢镀琉?

上文提到閉包的重要特性:封存上下文峦嗤,這一特性可以巧妙的被用于現(xiàn)有函數(shù)的包裝蕊唐,從而為現(xiàn)有函數(shù)增加功能。而這就是裝飾器烁设。

還是舉個(gè)例子替梨,代碼如下:

#alist = [1, 2, 3, ..., 100]  --> 1+2+3+...+100 = 5050
def lazy_sum():
    return reduce(lambda x, y: x+y, alist)

我們定義了一個(gè)函數(shù)lazy_sum,作用是對alist中的所有元素求和后返回装黑。alist假設(shè)為1到100的整數(shù)列表:
alist = range(1, 101)

但是出于某種原因副瀑,我并不想馬上返回計(jì)算結(jié)果,而是在之后的某個(gè)地方恋谭,通過顯示的調(diào)用輸出結(jié)果糠睡。于是我用一個(gè)wrapper函數(shù)對其進(jìn)行包裝:

def wrapper():
    alist = range(1, 101)
    def lazy_sum():
        return reduce(lambda x, y: x+y, alist)
    return lazy_sum

lazy_sum = wrapper()        #wrapper() 返回的是lazy_sum函數(shù)對象

if __name__  == "__main__":
    lazy_sum()           #5050

這是一個(gè)典型的Lazy Evaluation的例子。我們知道疚颊,一般情況下狈孔,局部變量在函數(shù)返回時(shí),就會被垃圾回收器回收材义,而不能再被使用均抽。但是這里的alist卻沒有,它隨著lazy_sum函數(shù)對象的返回被一并返回了(這個(gè)說法不準(zhǔn)確其掂,實(shí)際是包含在了lazy_sum的執(zhí)行環(huán)境中油挥,通過_globals_),從而延長了生命周期款熬。

當(dāng)在if語句塊中調(diào)用lazy_sum()的時(shí)候深寥,解析器會從上下文中(這里是Enclosing層的wrapper函數(shù)的局部作用域中)找到alist列表,計(jì)算結(jié)果贤牛,返回5050惋鹅。

當(dāng)你需要?jiǎng)討B(tài)的給已定義的函數(shù)增加功能時(shí),比如:參數(shù)檢查盔夜,類似的原理就變得很有用:

def add(a, b):
    return a+b

這是很簡單的一個(gè)函數(shù):計(jì)算a+b的和返回负饲,但我們知道Python是 動(dòng)態(tài)類型+強(qiáng)類型 的語言堤魁,你并不能保證用戶傳入的參數(shù)a和b一定是兩個(gè)整型,他有可能傳入了一個(gè)整型和一個(gè)字符串類型的值:

In [2]: add(1, 2)
Out[2]: 3

In [3]: add(1.2, 3.45)
Out[3]: 4.65

In [4]: add(5, 'hello')
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/home/chiyu/<ipython-input-4-f2f9e8aa5eae> in <module>()
----> 1 add(5, 'hello')

/home/chiyu/<ipython-input-1-02b3d3d6caec> in add(a, b)
      1 def add(a, b):
----> 2     return a+b

TypeError: unsupported operand type(s) for +: 'int' and 'str'

于是返十,解析器無情的拋出了一個(gè)TypeError異常妥泉。

動(dòng)態(tài)類型:在運(yùn)行期間確定變量的類型,python確定一個(gè)變量的類型是在你第一次給他賦值的時(shí)候洞坑;
強(qiáng)類型:有強(qiáng)制的類型定義盲链,你有一個(gè)整數(shù),除非顯示的類型轉(zhuǎn)換迟杂,否則絕不能將它當(dāng)作一個(gè)字符串(例如直接嘗試將一個(gè)整型和一個(gè)字符串做+運(yùn)算)刽沾;

因此,為了更加優(yōu)雅的使用add函數(shù)排拷,我們需要在執(zhí)行+運(yùn)算前侧漓,對a和b進(jìn)行參數(shù)檢查。這時(shí)候裝飾器就顯得非常有用:

import logging

logging.basicConfig(level = logging.INFO)

def add(a, b):
    return a + b

def checkParams(fn):
    def wrapper(a, b):
        if isinstance(a, (int, float)) and isinstance(b, (int, float)):    #檢查參數(shù)a和b是否都為整型或浮點(diǎn)型
            return fn(a, b)             #是則調(diào)用fn(a, b)返回計(jì)算結(jié)果

        #否則通過logging記錄錯(cuò)誤信息监氢,并友好退出
        logging.warning("variable 'a' and 'b' cannot be added")   
        return
    return wrapper     #fn引用add布蔗,被封存在閉包的執(zhí)行環(huán)境中返回

if __name__ == "__main__":
    #將add函數(shù)對象傳入,fn指向add
    #等號左側(cè)的add浪腐,指向checkParams的返回值wrapper
    add = checkParams(add)      
    add(3, 'hello')     #經(jīng)過類型檢查纵揍,不會計(jì)算結(jié)果,而是記錄日志并退出

注意checkParams函數(shù):

  • 首先看參數(shù)fn议街,當(dāng)我們調(diào)用checkParams(add)的時(shí)候泽谨,它將成為函數(shù)對象add的一個(gè)本地(Local)引用;
  • 在checkParams內(nèi)部特漩,我們定義了一個(gè)wrapper函數(shù)吧雹,添加了參數(shù)類型檢查的功能,然后調(diào)用了fn(a, b)拾稳,根據(jù)LEGB法則吮炕,解釋器將搜索幾個(gè)作用域,并最終在(Enclosing層)checkParams函數(shù)的本地作用域中找到fn访得;
  • 注意最后的return wrapper龙亲,這將創(chuàng)建一個(gè)閉包,fn變量(add函數(shù)對象的一個(gè)引用)將會封存在閉包的執(zhí)行環(huán)境中悍抑,不會隨著checkParams的返回而被回收鳄炉;

當(dāng)調(diào)用add = checkParams(add)時(shí),add指向了新的wrapper對象搜骡,它添加了參數(shù)檢查和記錄日志的功能拂盯,同時(shí)又能夠通過封存的fn,繼續(xù)調(diào)用原始的add進(jìn)行+運(yùn)算记靡。

因此調(diào)用add(3, 'hello')將不會返回計(jì)算結(jié)果谈竿,而是打印出日志:

chiyu@chiyu-PC:~$ python func.py 
WARNING:root:variable 'a' and 'b' cannot be added

有人覺得add = checkParams(add)這樣的寫法未免太過麻煩团驱,于是python提供了一種更優(yōu)雅的寫法,被稱為語法糖:

@checkParams
def add(a, b):
    return a + b

這只是一種寫法上的優(yōu)化空凸,解釋器仍然會將它轉(zhuǎn)化為add = checkParams(add)來執(zhí)行嚎花。

?

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市呀洲,隨后出現(xiàn)的幾起案子紊选,更是在濱河造成了極大的恐慌,老刑警劉巖道逗,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件兵罢,死亡現(xiàn)場離奇詭異,居然都是意外死亡滓窍,警方通過查閱死者的電腦和手機(jī)卖词,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來贰您,“玉大人坏平,你說我怎么就攤上這事〗跻啵” “怎么了?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵令境,是天一觀的道長杠园。 經(jīng)常有香客問我,道長舔庶,這世上最難降的妖魔是什么抛蚁? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮惕橙,結(jié)果婚禮上瞧甩,老公的妹妹穿的比我還像新娘。我一直安慰自己弥鹦,他們只是感情好肚逸,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著彬坏,像睡著了一般朦促。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上栓始,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天务冕,我揣著相機(jī)與錄音,去河邊找鬼幻赚。 笑死禀忆,一個(gè)胖子當(dāng)著我的面吹牛臊旭,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播箩退,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼巍扛,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了乏德?” 一聲冷哼從身側(cè)響起撤奸,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎喊括,沒想到半個(gè)月后胧瓜,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡郑什,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年府喳,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蘑拯。...
    茶點(diǎn)故事閱讀 38,654評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡钝满,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出申窘,到底是詐尸還是另有隱情弯蚜,我是刑警寧澤,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布剃法,位于F島的核電站碎捺,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏贷洲。R本人自食惡果不足惜收厨,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望优构。 院中可真熱鬧诵叁,春花似錦、人聲如沸钦椭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽玉凯。三九已至势腮,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間漫仆,已是汗流浹背捎拯。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人署照。 一個(gè)月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓祸泪,卻偏偏與公主長得像,于是被迫代替她去往敵國和親建芙。 傳聞我的和親對象是個(gè)殘疾皇子没隘,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,543評論 2 349

推薦閱讀更多精彩內(nèi)容