Python裝飾器(Decorator)完全指南-高級篇

引言

通過前面兩篇文章(前兩篇文章見基礎(chǔ)篇, 進(jìn)階篇),讀者們已經(jīng)了解了到了python中的裝飾器背后的實現(xiàn)邏輯舰褪,如何理解python中以@標(biāo)記的帶裝飾器函數(shù)皆疹,以及如何構(gòu)造帶參數(shù)函數(shù)的裝飾器和動態(tài)生成裝飾器。在接下來這篇文章中占拍,我們將應(yīng)用之前的知識來研究一個比較復(fù)雜的問題——如何構(gòu)造裝飾器的裝飾器,以及裝飾器的最佳實踐捎迫。

構(gòu)造裝飾器的裝飾器

在進(jìn)階篇中我們已經(jīng)知道了如何構(gòu)造一個能接受任意參數(shù)的裝飾器函數(shù)晃酒,同時我們還能夠通過構(gòu)造裝飾器工廠的方式來動態(tài)根據(jù)輸入?yún)?shù)生成裝飾器。接下來我們將應(yīng)用這些知識來實現(xiàn)裝飾器的裝飾器窄绒。這一裝飾器能夠用來裝飾其他裝飾器函數(shù)贝次,從而使得被裝飾的裝飾器函數(shù)能夠接收任意輸入?yún)?shù)(請認(rèn)真讀一讀前面這句邏輯不太直觀的句子,確認(rèn)你已經(jīng)理解了下面代碼的目的)彰导。

這一代碼的有用之處在于蛔翅,我們能夠動態(tài)地將我們的任意一個裝飾器變?yōu)橐粋€能夠接收參數(shù)的裝飾器工廠敲茄。如進(jìn)階篇中所述,由于裝飾器的函數(shù)簽名是固定的——def decorator_func(func_to_decorate)山析,我們無法在使用@調(diào)用裝飾器函數(shù)的時候動態(tài)傳入?yún)?shù)堰燎,所以只能先定義一個裝飾器工廠,來替我們接收參數(shù)并返回包含了參數(shù)的閉環(huán)(也就是裝飾器)笋轨。而下面的裝飾器將這一功能抽象了出來秆剪,使得其可以復(fù)用。

代碼如下所示爵政。

def decorator_for_decorator(decorator_to_enhance):
    """
    這一函數(shù)用來作為一個裝飾器工廠來動態(tài)生成裝飾器仅讽。
    生成的裝飾器能夠被用來裝飾其他裝飾器函數(shù),使得被裝飾的裝飾器函數(shù)變?yōu)槟軌蛉我饨邮諈?shù)的裝飾器钾挟。
    """
    # 為了實現(xiàn)參數(shù)的傳遞洁灵,我們在這里動態(tài)生成了一個裝飾器工廠,并作為返回值
    # 這一裝飾器工廠將返回一個閉環(huán)作為裝飾器掺出,其中包裝了外界傳入的裝飾器參數(shù)
    def decorator_maker(*args, **kwargs):
        def decorator_wrapper(func):
            # 這里使用了閉環(huán)來保證參數(shù)的傳遞
            return decorator_to_enhance(func, *args, **kwargs)

        return decorator_wrapper
    return decorator_maker

有了這一裝飾器之后处渣,我們就可以動態(tài)改變其他裝飾器了。

# 注意為了保證我們所包裝的裝飾器能夠正確接收參數(shù)蛛砰,我們需要保證其函數(shù)簽名包含我們想要傳入的參數(shù)
# 但這樣的裝飾器函數(shù)是無法直接用來裝飾其他函數(shù)的
@decorator_for_decorator
def decorator_func(func, *args, **kwargs):
    def wrapper(function_arg1, function_arg2):
        print('Received arguments as {}, {}'.format(args, kwargs))
        print('${} per {}, ${} per {}, what would you like to have?'.format(args[0], function_arg1, args[1], function_arg2))
        return func(function_arg1, function_arg2)
    return wrapper

# 此時罐栈,我們就可以給我們的裝飾器傳入?yún)?shù)啦
@decorator_func(3, 5)
def func(function_arg1, function_arg2):
    print('Hello, I would like to have {} and {}'.format(function_arg1, function_arg2))

func('ice cream', 'pizza')
# output:
# Received arguments as (3, 5), {}
# $3 per ice cream, $5 per pizza, what would you like to have?
# Hello, I would like to have ice cream and pizza

上面的代碼可能邏輯上不是那么直觀。我們接下來詳細(xì)分析泥畅。

首先我們定義了一個叫做decorator_maker的裝飾器函數(shù)荠诬。這一函數(shù)實質(zhì)上是一個能夠返回工廠函數(shù)的函數(shù)。它返回一個能夠返回裝飾器的工廠函數(shù)位仁。也就是說柑贞,被這一裝飾器裝飾過之后,原有的裝飾器函數(shù)將變?yōu)橐粋€裝飾器工廠函數(shù)聂抢。而這一工廠函數(shù)钧嘶,正如我們在進(jìn)階篇中所述,用來接收我們想要傳入的參數(shù)并通過返回一個動態(tài)生成的閉環(huán)作為裝飾器的方式來實現(xiàn)將參數(shù)傳入裝飾器中的目的琳疏。緊接著我們就可以帶參數(shù)通過調(diào)用這個工廠函數(shù)的方式來實現(xiàn)裝飾一個函數(shù)的過程有决。

通過展開裝飾器的方式來理解裝飾的過程

進(jìn)一步地,我們總是可以通過展開裝飾器的方式來理解裝飾的過程發(fā)生了什么空盼。這一方法可以用來分析所有的裝飾器裝飾過的函數(shù)书幕。以上面的代碼為例。

# 我們使用@decorator_for_decorator的方法裝飾了decorator_func函數(shù)揽趾√ɑ悖可以展開如下
def decorator_func(func, *args, **kwargs):
    def wrapper(function_arg1, function_arg2):
        print('Received arguments as {}, {}'.format(args, kwargs))
        print('${} per {}, ${} per {}, what would you like to have?'.format(args[0], function_arg1, args[1], function_arg2))
        return func(function_arg1, function_arg2)
    return wrapper
decorator_func = decorator_for_decorator(decorator_func)
# 經(jīng)過上面的步驟,decorator_func引用所指向的其實已經(jīng)是decorator_for_decorator所返回的decorator_maker函數(shù)了。這一函數(shù)是一個裝飾器工廠函數(shù)苟呐。

# 緊接著我們帶參數(shù)調(diào)用這一工廠函數(shù)(@decorator_func(3,5))并使用其返回的裝飾器函數(shù)來裝飾另一個函數(shù)(func)痒芝。這一過程展開如下。
true_decorator = decorator_func(3,5)
# 上面的true_decorator引用指向的是decorator_maker所返回的閉環(huán)decorator_wrapper牵素。

def func(function_arg1, function_arg2):
    print('Hello, I would like to have {} and {}'.format(function_arg1, function_arg2))
func = true_decorator(func)
# 到上面這一步為止严衬,我們已經(jīng)完成了裝飾func的任務(wù),并且將參數(shù)通過閉環(huán)的方式傳入了我們所使用的裝飾器中两波。

裝飾器的最佳實踐

  • 裝飾器實在python2.4中被引入的瞳步。所以要使用裝飾器,需要確保我們使用的python版本>=2.4腰奋。
  • 使用裝飾器會減慢調(diào)用函數(shù)的速度单起。
  • 一旦一個函數(shù)被裝飾過之后,我們就無法在運(yùn)行時再調(diào)用未裝飾過的原函數(shù)了劣坊。
  • 裝飾器事實上只是一個接受函數(shù)作為輸入的函數(shù)嘀倒,并返回一個對原函數(shù)的包裝函數(shù)。這一包裝過程可能會使得debug過程更加復(fù)雜和困難局冰。但在2.5(含)之后的python版本中我們可以使用functools.wraps()來降低裝飾器對debug的影響测蘑。

python從2.5開始引入了functools模塊(module),其中包含的裝飾器函數(shù)functools.wraps()能夠保證被裝飾函數(shù)的函數(shù)名康二,模塊名碳胳,以及文檔字符串(docstring)被傳入裝飾器返回的包裝函數(shù)中,從而保證了拋出的錯誤信息中能夠包含正確的函數(shù)名沫勿,改善了debug體驗挨约。如下面的代碼所示。

# python的stacktrace信息中包含函數(shù)的__name__屬性來幫助debug
def foo():
    pass
print(foo.__name__)
# output: foo

# 但是用了裝飾器之后产雹,__name__屬性會發(fā)生變化
def bar(func):
    def wrapper():
        return func()
    return wrapper

@bar
def foo():
    pass
print(foo.__name__)
# output: wrapper

# 通過使用functools來改變這一狀況
import functools
def bar(func):
    @functools.wraps(func)
    def wrapper():
        return func()
    return wrapper

@bar
def foo():
    pass

print(foo.__name__)
# output: foo

那么诫惭,裝飾器到底有什么用呢?

裝飾器的用法多種多樣蔓挖。舉例來說夕土,我們?nèi)绻胍獢U(kuò)展一個第三方庫中附帶的函數(shù)的功能,但我們又無法修改該函數(shù)源代碼的時候瘟判,我們就可以使用裝飾器來實現(xiàn)這一目的怨绣。或者我們在debug的時候荒适,為了避免對源代碼進(jìn)行多次修改梨熙,就可以用裝飾器來附加我們想要的邏輯。換句話說刀诬,我們可以用裝飾器實現(xiàn)所謂的“干修改”(Dry Change)。

import time
import functools


def benchmark(func):
    """
    這是一個能夠計算并打印一個函數(shù)運(yùn)行時間的裝飾器
    """
    def wrapper(*args, **kwargs):
        start_time = time.time()
        res = func(*args, **kwargs)
        end_time = time.time()
        print('{} completed in {} seconds'.format(func.__name__,  end_time - start_time))
        return res
    return wrapper


def logging(func):
    """
    這是一個能夠附加log功能的裝飾器
    """
    def wrapper(*args, **kwargs):
        res = func(*args, **kwargs)
        print('{} executed with args: {} and kwargs: {}'.format(func.__name__, args, kwargs))
        return res
    return wrapper


def counter(func):
    """
    這是一個能夠?qū)瘮?shù)被調(diào)用次數(shù)進(jìn)行計數(shù)的裝飾器
    """
    def wrapper(*args, **kwargs):
        wrapper.count = wrapper.count + 1
        res = func(*args, **kwargs)
        print('{} has been called for {} times'.format(func.__name__, wrapper.count))
        return res
    wrapper.count = 0
    return wrapper


@counter
@logging
@benchmark
def reverse_string(string):
    return ''.join(reversed(string))


reverse_string('Tough times do not last, tough people do.')
# output:
# reverse_string completed in 3.814697265625e-06 seconds
# reverse_string executed with args: ('Tough times do not last, tough people do.',) and kwargs: {}
# reverse_string has been called for 1 times
# '.od elpoep hguot ,tsal ton od semit hguoT'

reverse_string('Two things are infinite: the universe and human stupidity; and I am not sure about the universe.')
# reverse_string completed in 5.9604644775390625e-06 seconds
# reverse_string executed with args: ('Two things are infinite: the universe and human stupidity; and I am not sure about the universe.',) and kwargs: {}
# reverse_string has been called for 2 times
# '.esrevinu eht tuoba erus ton ma I dna ;ytidiputs namuh dna esrevinu eht :etinifni era sgniht owT'

實際上,python自身也提供了一些常用的裝飾器供大家調(diào)用陕壹,例如property质欲,staticmethod,等等糠馆。與此同時嘶伟,一些常用的python后端框架,例如DjangoPyramid也使用裝飾器來管理緩存以及視圖(view)訪問權(quán)限等又碌。另外九昧,裝飾器有時候也用來在測試中來虛構(gòu)異步請求。

總之毕匀,裝飾器在實際開發(fā)中可以有許多靈活的應(yīng)用铸鹰。讀者朋友們今后可以多加嘗試。

Reference
文中部分內(nèi)容翻譯自如下文章皂岔。翻譯部分版權(quán)歸原作者所有蹋笼。
https://gist.github.com/Zearin/2f40b7b9cfc51132851a

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市躁垛,隨后出現(xiàn)的幾起案子剖毯,更是在濱河造成了極大的恐慌,老刑警劉巖教馆,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件逊谋,死亡現(xiàn)場離奇詭異,居然都是意外死亡土铺,警方通過查閱死者的電腦和手機(jī)胶滋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來舒憾,“玉大人镀钓,你說我怎么就攤上這事《朴兀” “怎么了丁溅?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長探遵。 經(jīng)常有香客問我窟赏,道長,這世上最難降的妖魔是什么箱季? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任涯穷,我火速辦了婚禮,結(jié)果婚禮上藏雏,老公的妹妹穿的比我還像新娘拷况。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布赚瘦。 她就那樣靜靜地躺著粟誓,像睡著了一般。 火紅的嫁衣襯著肌膚如雪起意。 梳的紋絲不亂的頭發(fā)上鹰服,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天,我揣著相機(jī)與錄音揽咕,去河邊找鬼悲酷。 笑死,一個胖子當(dāng)著我的面吹牛亲善,可吹牛的內(nèi)容都是我干的设易。 我是一名探鬼主播,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼逗爹,長吁一口氣:“原來是場噩夢啊……” “哼亡嫌!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起掘而,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤挟冠,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后袍睡,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體知染,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年斑胜,在試婚紗的時候發(fā)現(xiàn)自己被綠了控淡。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡止潘,死狀恐怖掺炭,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情凭戴,我是刑警寧澤涧狮,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站么夫,受9級特大地震影響者冤,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜档痪,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一涉枫、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧腐螟,春花似錦愿汰、人聲如沸困后。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽操灿。三九已至锯仪,卻和暖如春泵督,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背庶喜。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工小腊, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人久窟。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓秩冈,卻偏偏與公主長得像,于是被迫代替她去往敵國和親斥扛。 傳聞我的和親對象是個殘疾皇子入问,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,792評論 2 345