談?wù)勓b飾器的實(shí)現(xiàn)原理

關(guān)于我
一個(gè)有思想的程序猿,終身學(xué)習(xí)實(shí)踐者肛度,目前在一個(gè)創(chuàng)業(yè)團(tuán)隊(duì)任team lead傻唾,技術(shù)棧涉及Android、Python承耿、Java和Go冠骄,這個(gè)也是我們團(tuán)隊(duì)的主要技術(shù)棧。
Github:https://github.com/hylinux1024
微信公眾號(hào):終身開發(fā)者(angrycode)

談?wù)勓b飾器(Decorator)的實(shí)現(xiàn)原理

熟悉Java編程的程序猿對(duì)裝飾器模式一定不陌生加袋,它是能夠動(dòng)態(tài)的給一個(gè)類添加新的行為的一種設(shè)計(jì)模式凛辣。相對(duì)于通過繼承的方式使用裝飾器會(huì)更加靈活。

圖片來源wiki百科

Python里面裝飾器(Decorator)也是一個(gè)非常重要的概念职烧。跟裝飾器模式類似扁誓,它能夠動(dòng)態(tài)為一個(gè)函數(shù)防泵、方法或者類添加新的行為,而不需要通過子類繼承或直接修改函數(shù)的代碼來獲取新的行為能力蝗敢,使用Decorator的方式會(huì)更加Pythonic择克。

要理解裝飾器我們就要從函數(shù)說起。

0x00 函數(shù)

Python中函數(shù)是作為一級(jí)對(duì)象存在的(一切都是對(duì)象)前普,它擁有自己的屬性肚邢,函數(shù)名可以賦值給一個(gè)變量,也可以作為另一個(gè)函數(shù)的參數(shù)進(jìn)行傳遞拭卿。

1骡湖、定義函數(shù)
def fib(n):
    """打印小于 n 的 fibonacci 數(shù)列"""
    a, b = 0, 1
    while a < n:
        print(a, end=' ')
        a, b = b, a + b
    print()

def call_fib(func):
    """函數(shù)名作為函數(shù)的參數(shù)進(jìn)行傳遞"""
    func(1000)

if __name__ == '__main__':
    print(fib)  # <function fib at 0x103e66378>
    print(isinstance(fib, object))  # 函數(shù)是一級(jí)對(duì)象:True
    print(fib.__name__)  # 函數(shù)名稱:fib
    print(fib.__code__.co_varnames)  # __code__屬性是函數(shù)中的'代碼對(duì)象',co_varnames是函數(shù)中的本地變量峻厚,以元組的方式展現(xiàn):('n', 'a', 'b')
    print(fib.__doc__)  # 函數(shù)中的注釋 
    print(fib.__globals__)  # 全局變量
    
    f = fib  # 函數(shù)賦值給一個(gè)變量f
    f(1000)  # 通過變量f對(duì)函數(shù)fib調(diào)用
    
    call_fib(fib) # 函數(shù)名作為參數(shù)
2响蕴、嵌套函數(shù)

在定義函數(shù)時(shí),在函數(shù)內(nèi)部定義一個(gè)函數(shù)惠桃。

def outer_func():
    # 在函數(shù)內(nèi)部定義一個(gè)函數(shù)
    def inner_func():
        print('inner func')

    inner_func()
    print('outer func')

嵌套函數(shù)對(duì)我們理解裝飾器非常重要浦夷,也是閉包實(shí)現(xiàn)的基礎(chǔ),這里先引出了本地變量和全局變量的概念辜王,后文會(huì)詳細(xì)說明閉包的概念劈狐。

3、全局變量(globals)和本地變量(locals)

根據(jù)作用域的不同呐馆,變量可以分成全局變量和本地變量肥缔,這其實(shí)是相對(duì)的概念。例如在下面的模塊中gvar是一個(gè)全局變量汹来,而在函數(shù)outer_func()定義的是本地變量续膳。

gvar = 'global var' # 全局變量


def outer_func():
    gvar = 'outer var' # outer_func 本地變量

    # 在函數(shù)內(nèi)部定義一個(gè)函數(shù)
    def inner_func():
        gvar = 'inner var' # inner_func 本地變量
        print('inner: {}'.format(gvar))

    inner_func()
    print('outer: {}'.format(gvar))

outer_func()
print('gvar in global : {}'.format(gvar))
# 輸出結(jié)果
# inner: inner var
# outer: outer var
# gvar in global : global var

在函數(shù)外定義了全局變量gvar,同時(shí)在outer_func()函數(shù)中也定義了gvar收班,而這個(gè)是本地變量坟岔。
從示例代碼中可以看到,outer_func()并沒有改變?nèi)肿兞康闹怠?/p>

在函數(shù)中定義的變量都存儲(chǔ)在本地符號(hào)表(local symbol table)里摔桦,同樣inner_func()中的gvar也存在它自己的本地符號(hào)表中社付,而全局變量gvar是則存儲(chǔ)在全局符號(hào)表(global symbol table)。
變量的查找路是:首先從本地符號(hào)表中查找酣溃,然后是外部函數(shù)(在嵌套函數(shù)中)的本地符號(hào)表中查找瘦穆,再到全局符號(hào)表,最后是內(nèi)置符號(hào)表

graph TD
A[本地符號(hào)表]-->B[外部函數(shù)的本地符號(hào)表]
B[函數(shù)外的本地符號(hào)表]-->C[全局符號(hào)表]
C[全局符號(hào)表]-->D[內(nèi)置符號(hào)表]

如果把上面代碼中的inner_func()中的gvar = 'inner var'注釋掉赊豌,那么輸出的結(jié)果將是

# inner: outer gvar # inner_func中引用的gvar變量是outer_func中定義的
# outer: outer gvar
# gvar in global : global var

變量查找邏輯可以簡(jiǎn)單理解為:就近查找
如果在以上路徑中都沒有找到绵咱,Python解析器將拋出NameError: name 'gvar' is not defined碘饼。

若在函數(shù)中要使用全局變量熙兔,那么就需要用到global關(guān)鍵字。
對(duì)上文的代碼修改如下

gvar = 'global var'


def outer_func():
    global gvar  # 聲明gvar是全局變量
    gvar = 'outer gvar'

    # 在函數(shù)內(nèi)部定義一個(gè)函數(shù)
    def inner_func():
        gvar = 'inner gvar'  # 這個(gè)依然是本地變量
        print('inner: {}'.format(gvar))

    inner_func()
    print('outer: {}'.format(gvar))

outer_func()
print('gvar in global : {}'.format(gvar))

# 輸出結(jié)果
# inner: inner gvar
# outer: outer gvar
# gvar in global : outer gvar

除了global還有一個(gè)nonlocal的關(guān)鍵字艾恼,它的作用是在函數(shù)中使用外部函數(shù)的變量定義(注意:不能是全局變量)

gvar = 'global var' # 全局變量


def outer_func():
    gvar = 'outer gvar' # 本地變量
    # 在函數(shù)內(nèi)部定義一個(gè)函數(shù)
    def inner_func():
        nonlocal gvar  # nonlocal的含義是讓gvar使用外部函數(shù)的變量住涉,
        # 如果外部函數(shù)沒有定義該變量,那么運(yùn)行時(shí)將拋出SyntaxError: no binding for nonlocal 'gvar' found
        gvar = 'inner gvar'  # 這個(gè)依然是本地變量
        print('inner: {}'.format(gvar))

    inner_func()
    print('outer: {}'.format(gvar))

# 輸出結(jié)果
# inner: inner gvar
# outer: inner gvar
# gvar in global : global var

inner_func()中使用nonlocal關(guān)鍵字聲明的gvar必須在外部函數(shù)(即outer_func()函數(shù))定義钠绍,否則將拋出SyntaxError: no binding for nonlocal 'gvar' found

0x01 什么是閉包

首先結(jié)合前文的嵌套函數(shù)定義的例子舆声,修改一下代碼,返回內(nèi)部函數(shù)的對(duì)象柳爽。

# 普通的嵌套函數(shù)
def outer_func():
    # 在函數(shù)內(nèi)部定義一個(gè)函數(shù)
    def inner_func():
        print('inner func')

    inner_func()
    print('outer func')


# 閉包
def closure_func():
    local_var = 100

    def inner_func():
        print('inner func call : {}'.format(local_var))

    return inner_func # 這里將形成一個(gè)閉包

f = closure_func()
print(f)
print(f.__closure__)
print(outer_func)
print(outer_func.__closure__)


# 輸出結(jié)果
# <function closure_func.<locals>.inner_func at 0x104f8a8c8> 
# (<cell at 0x104f6fa68: int object at 0x104d23910>,)
# <function outer_func at 0x1070ea268>
# None # 普通函數(shù)的__closure__屬性為空

可以看出變量f就是閉包媳握,它是一個(gè)函數(shù)對(duì)象,這個(gè)函數(shù)可以持有本地變量local_var磷脯,而這個(gè)本地變量可以脫離定義它的作用域而存在蛾找。

現(xiàn)在來維基百科關(guān)于閉包的定義

在計(jì)算機(jī)科學(xué)中,閉包(英語:Closure)赵誓,又稱詞法閉包(Lexical Closure)或函數(shù)閉包(function closures)打毛,是引用了自由變量的函數(shù)。這個(gè)被引用的自由變量將和這個(gè)函數(shù)一同存在俩功,即使已經(jīng)離開了創(chuàng)造它的環(huán)境也不例外幻枉。--引用自維基百科

0x02 實(shí)現(xiàn)裝飾器

有了前面的鋪墊,理解裝飾器就非常簡(jiǎn)單啦诡蜓。
一個(gè)函數(shù)返回另外一個(gè)函數(shù)展辞,通常會(huì)使用@wrapper的語法形式,而裝飾器其實(shí)就是一種語法糖(syntactic sugar)万牺。
我們還是看代碼

# 定義一個(gè)logger函數(shù)罗珍,在函數(shù)執(zhí)行前打印log信息
def logger(func):
    def log_func(*args):
        print('Running "{}" with arguments {}'.format(func.__name__, args))
        return func(*args)

    return log_func # 形成閉包

# 定義加法函數(shù)
def add(x, y):
    return x + y

# 以下兩種方式的使用是等價(jià)的,當(dāng)然使用@logger更加Pythonic
@logger
def add(x, y):
    return x + y


# add = logger(add)

print(add(1,4))

# 輸出結(jié)果
# Running "add" with arguments (1, 4)
# 5

這樣的通過自定義裝飾器脚粟,我們就可以動(dòng)態(tài)地給函數(shù)添加新的功能覆旱。
除了自定義的裝飾器,還有常見的如classmethod()staticmethod()內(nèi)置的裝飾器核无。

0x03 總結(jié)

本文重點(diǎn)說明函數(shù)和嵌套函數(shù)的定義扣唱,還說明了全局變量和本地變量的作用域,在Python中變量索引的路徑是就近查找团南,同時(shí)引出閉包是一個(gè)持有自由變量的函數(shù)對(duì)象的概念噪沙,而通過閉包可以實(shí)現(xiàn)裝飾器,在使用使用裝飾器時(shí)吐根,可以使用@wrapper形式的語法糖正歼。

0x04 引用

  1. https://docs.python.org/3/reference/compound_stmts.html#function-definitions
  2. https://wiki.python.org/moin/PythonDecorators
  3. https://docs.python.org/3/tutorial/controlflow.html#defining-functions
  4. https://zh.wikipedia.org/wiki/%E9%97%AD%E5%8C%85_(%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市拷橘,隨后出現(xiàn)的幾起案子局义,更是在濱河造成了極大的恐慌喜爷,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,627評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件萄唇,死亡現(xiàn)場(chǎng)離奇詭異檩帐,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)另萤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,180評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門湃密,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人四敞,你說我怎么就攤上這事泛源。” “怎么了目养?”我有些...
    開封第一講書人閱讀 169,346評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵俩由,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我癌蚁,道長(zhǎng)幻梯,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,097評(píng)論 1 300
  • 正文 為了忘掉前任努释,我火速辦了婚禮碘梢,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘伐蒂。我一直安慰自己煞躬,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,100評(píng)論 6 398
  • 文/花漫 我一把揭開白布逸邦。 她就那樣靜靜地躺著恩沛,像睡著了一般。 火紅的嫁衣襯著肌膚如雪缕减。 梳的紋絲不亂的頭發(fā)上雷客,一...
    開封第一講書人閱讀 52,696評(píng)論 1 312
  • 那天,我揣著相機(jī)與錄音桥狡,去河邊找鬼搅裙。 笑死,一個(gè)胖子當(dāng)著我的面吹牛裹芝,可吹牛的內(nèi)容都是我干的部逮。 我是一名探鬼主播,決...
    沈念sama閱讀 41,165評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼嫂易,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼兄朋!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起炬搭,我...
    開封第一講書人閱讀 40,108評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤蜈漓,失蹤者是張志新(化名)和其女友劉穎穆桂,沒想到半個(gè)月后宫盔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體融虽,經(jīng)...
    沈念sama閱讀 46,646評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,709評(píng)論 3 342
  • 正文 我和宋清朗相戀三年灼芭,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了有额。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,861評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡彼绷,死狀恐怖巍佑,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情寄悯,我是刑警寧澤萤衰,帶...
    沈念sama閱讀 36,527評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站猜旬,受9級(jí)特大地震影響脆栋,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜洒擦,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,196評(píng)論 3 336
  • 文/蒙蒙 一椿争、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧熟嫩,春花似錦秦踪、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,698評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至昧狮,卻和暖如春景馁,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背陵且。 一陣腳步聲響...
    開封第一講書人閱讀 33,804評(píng)論 1 274
  • 我被黑心中介騙來泰國打工裁僧, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人慕购。 一個(gè)月前我還...
    沈念sama閱讀 49,287評(píng)論 3 379
  • 正文 我出身青樓聊疲,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親沪悲。 傳聞我的和親對(duì)象是個(gè)殘疾皇子获洲,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,860評(píng)論 2 361

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

  • 這是16年5月份編輯的一份比較雜亂適合自己觀看的學(xué)習(xí)記錄文檔,今天18年5月份再次想寫文章殿如,發(fā)現(xiàn)簡(jiǎn)書還為我保存起的...
    Jenaral閱讀 2,771評(píng)論 2 9
  • 部分細(xì)節(jié)自己改了點(diǎn)贡珊,也加了點(diǎn)自己例子最爬,基本上屬于轉(zhuǎn)載。轉(zhuǎn)載出處:https://my.oschina.net/le...
    洛克黃瓜閱讀 1,983評(píng)論 0 3
  • 呵呵门岔!作為一名教python的老師爱致,我發(fā)現(xiàn)學(xué)生們基本上一開始很難搞定python的裝飾器,也許因?yàn)檠b飾器確實(shí)很難懂...
    TypingQuietly閱讀 19,559評(píng)論 26 186
  • 今天主要對(duì)兩點(diǎn)比較有感觸寒随,一是人總是過度自信:低估對(duì)方的實(shí)力糠悯,高估自己的能力。二是人類很擅長(zhǎng)遺忘痛苦妻往。先說第一點(diǎn)互艾,...
    藝涵JS閱讀 740評(píng)論 0 0
  • 采藥的少女救下了跌落山崖的他,細(xì)心照料讯泣,終于他逐漸恢復(fù)了健康纫普。 “我要走了『们”他向采藥的女孩告別昨稼。...
    白芷啊閱讀 696評(píng)論 3 14