Python 裝飾器

每個人都有的內(nèi)褲主要功能是用來遮羞厉熟,但是到了冬天它沒法為我們防風(fēng)御寒孵坚,咋辦?我們想到的一個辦法就是把內(nèi)褲改造一下贼陶,讓它變得更厚更長蟆盹,這樣一來孩灯,它不僅有遮羞功能,還能提供保暖逾滥,不過有個問題峰档,這個內(nèi)褲被我們改造成了長褲后,雖然還有遮羞功能,但本質(zhì)上它不再是一條真正的內(nèi)褲了讥巡。于是聰明的人們發(fā)明長褲掀亩,在不影響內(nèi)褲的前提下,直接把長褲套在了內(nèi)褲外面尚卫,這樣內(nèi)褲還是內(nèi)褲归榕,有了長褲后寶寶再也不冷了。裝飾器就像我們這里說的長褲吱涉,在不影響內(nèi)褲作用的前提下刹泄,給我們的身子提供了保暖的功效。

談裝飾器前怎爵,還要先要明白一件事特石,Python 中的函數(shù)和 Java、C++不太一樣鳖链,Python 中的函數(shù)可以像普通變量一樣當(dāng)做參數(shù)傳遞給另外一個函數(shù)姆蘸,例如:

def foo():

print("foo")

def bar(func):

func()

bar(foo)

正式回到我們的主題。裝飾器本質(zhì)上是一個 Python 函數(shù)或類芙委,它可以讓其他函數(shù)或類在不需要做任何代碼修改的前提下增加額外功能逞敷,裝飾器的返回值也是一個函數(shù)/類對象。它經(jīng)常用于有切面需求的場景灌侣,比如:插入日志推捐、性能測試、事務(wù)處理侧啼、緩存牛柒、權(quán)限校驗等場景,裝飾器是解決這類問題的絕佳設(shè)計痊乾。有了裝飾器皮壁,我們就可以抽離出大量與函數(shù)功能本身無關(guān)的雷同代碼到裝飾器中并繼續(xù)重用。概括的講哪审,裝飾器的作用就是為已經(jīng)存在的對象添加額外的功能蛾魄。

先來看一個簡單例子,雖然實際代碼可能比這復(fù)雜很多:

def foo():

print('i am foo')

現(xiàn)在有一個新的需求湿滓,希望可以記錄下函數(shù)的執(zhí)行日志畏腕,于是在代碼中添加日志代碼:

def foo():

print('i am foo')

logging.info("foo is running")

如果函數(shù) bar()、bar2() 也有類似的需求茉稠,怎么做?再寫一個 logging 在 bar 函數(shù)里把夸?這樣就造成大量雷同的代碼而线,為了減少重復(fù)寫代碼,我們可以這樣做,重新定義一個新的函數(shù):專門處理日志 膀篮,日志處理完之后再執(zhí)行真正的業(yè)務(wù)代碼

def use_logging(func):

logging.warn("%s is running" % func.__name__)

func()

def foo():

print('i am foo')

use_logging(foo)

這樣做邏輯上是沒問題的嘹狞,功能是實現(xiàn)了,但是我們調(diào)用的時候不再是調(diào)用真正的業(yè)務(wù)邏輯 foo 函數(shù)誓竿,而是換成了 use_logging 函數(shù)磅网,這就破壞了原有的代碼結(jié)構(gòu), 現(xiàn)在我們不得不每次都要把原來的那個 foo 函數(shù)作為參數(shù)傳遞給 use_logging 函數(shù)筷屡,那么有沒有更好的方式的呢涧偷?當(dāng)然有,答案就是裝飾器毙死。

簡單裝飾器

def use_logging(func):

def wrapper():

logging.warn("%s is running" % func.__name__)

return func()? # 把 foo 當(dāng)做參數(shù)傳遞進來時燎潮,執(zhí)行func()就相當(dāng)于執(zhí)行foo()

return wrapper

def foo():

print('i am foo')

foo = use_logging(foo)? # 因為裝飾器 use_logging(foo) 返回的時函數(shù)對象 wrapper,這條語句相當(dāng)于? foo = wrapper

foo()? ? ? ? ? ? ? ? ? # 執(zhí)行foo()就相當(dāng)于執(zhí)行 wrapper()

use_logging 就是一個裝飾器扼倘,它一個普通的函數(shù)确封,它把執(zhí)行真正業(yè)務(wù)邏輯的函數(shù) func 包裹在其中,看起來像 foo 被 use_logging 裝飾了一樣再菊,use_logging 返回的也是一個函數(shù)爪喘,這個函數(shù)的名字叫 wrapper。在這個例子中纠拔,函數(shù)進入和退出時 秉剑,被稱為一個橫切面,這種編程方式被稱為面向切面的編程绿语。

@ 語法糖

如果你接觸 Python 有一段時間了的話秃症,想必你對 @ 符號一定不陌生了,沒錯 @ 符號就是裝飾器的語法糖吕粹,它放在函數(shù)開始定義的地方种柑,這樣就可以省略最后一步再次賦值的操作。

def use_logging(func):

def wrapper():

logging.warn("%s is running" % func.__name__)

return func()

return wrapper

@use_logging

def foo():

print("i am foo")

foo()

如上所示匹耕,有了 @ 聚请,我們就可以省去foo = use_logging(foo)這一句了,直接調(diào)用 foo() 即可得到想要的結(jié)果稳其。你們看到了沒有驶赏,foo() 函數(shù)不需要做任何修改,只需在定義的地方加上裝飾器既鞠,調(diào)用的時候還是和以前一樣煤傍,如果我們有其他的類似函數(shù),我們可以繼續(xù)調(diào)用裝飾器來修飾函數(shù)嘱蛋,而不用重復(fù)修改函數(shù)或者增加新的封裝蚯姆。這樣五续,我們就提高了程序的可重復(fù)利用性,并增加了程序的可讀性龄恋。

裝飾器在 Python 使用如此方便都要歸因于 Python 的函數(shù)能像普通的對象一樣能作為參數(shù)傳遞給其他函數(shù)疙驾,可以被賦值給其他變量,可以作為返回值郭毕,可以被定義在另外一個函數(shù)內(nèi)它碎。

*args、**kwargs

可能有人問显押,如果我的業(yè)務(wù)邏輯函數(shù) foo 需要參數(shù)怎么辦扳肛?比如:

def foo(name):

print("i am %s" % name)

我們可以在定義 wrapper 函數(shù)的時候指定參數(shù):

def wrapper(name):

logging.warn("%s is running" % func.__name__)

return func(name)

return wrapper

這樣 foo 函數(shù)定義的參數(shù)就可以定義在 wrapper 函數(shù)中。這時煮落,又有人要問了敞峭,如果 foo 函數(shù)接收兩個參數(shù)呢?三個參數(shù)呢蝉仇?更有甚者旋讹,我可能傳很多個。當(dāng)裝飾器不知道 foo 到底有多少個參數(shù)時轿衔,我們可以用 *args 來代替:

def wrapper(*args):

logging.warn("%s is running" % func.__name__)

return func(*args)

return wrapper

如此一來沉迹,甭管 foo 定義了多少個參數(shù),我都可以完整地傳遞到 func 中去害驹。這樣就不影響 foo 的業(yè)務(wù)邏輯了鞭呕。這時還有讀者會問,如果 foo 函數(shù)還定義了一些關(guān)鍵字參數(shù)呢宛官?比如:

def foo(name, age=None, height=None):

print("I am %s, age %s, height %s" % (name, age, height))

這時葫松,你就可以把 wrapper 函數(shù)指定關(guān)鍵字函數(shù):

def wrapper(*args, **kwargs):

# args是一個數(shù)組,kwargs一個字典

logging.warn("%s is running" % func.__name__)

return func(*args, **kwargs)

return wrapper

帶參數(shù)的裝飾器

裝飾器還有更大的靈活性底洗,例如帶參數(shù)的裝飾器腋么,在上面的裝飾器調(diào)用中,該裝飾器接收唯一的參數(shù)就是執(zhí)行業(yè)務(wù)的函數(shù) foo 亥揖。裝飾器的語法允許我們在調(diào)用時珊擂,提供其它參數(shù),比如@decorator(a)费变。這樣摧扇,就為裝飾器的編寫和使用提供了更大的靈活性。比如挚歧,我們可以在裝飾器中指定日志的等級扛稽,因為不同業(yè)務(wù)函數(shù)可能需要的日志級別是不一樣的。

def use_logging(level):

def decorator(func):

def wrapper(*args, **kwargs):

if level == "warn":

logging.warn("%s is running" % func.__name__)

elif level == "info":

logging.info("%s is running" % func.__name__)

return func(*args)

return wrapper

return decorator

@use_logging(level="warn")

def foo(name='foo'):

print("i am %s" % name)

foo()

@use_logging(level=”warn”)等價于@decorator

類裝飾器

沒錯滑负,裝飾器不僅可以是函數(shù)庇绽,還可以是類锡搜,相比函數(shù)裝飾器,類裝飾器具有靈活度大瞧掺、高內(nèi)聚、封裝性等優(yōu)點凡傅。使用類裝飾器主要依靠類的__call__方法辟狈,當(dāng)使用 @ 形式將裝飾器附加到函數(shù)上時,就會調(diào)用此方法夏跷。

class Foo(object):

def __init__(self, func):

self._func = func

def __call__(self):

print ('class decorator runing')

self._func()

print ('class decorator ending')

@Foo

def bar():

print ('bar')

bar()

functools.wraps

使用裝飾器極大地復(fù)用了代碼哼转,但是他有一個缺點就是原函數(shù)的元信息不見了,比如函數(shù)的docstring槽华、__name__壹蔓、參數(shù)列表,先看例子:

# 裝飾器

def logged(func):

def with_logging(*args, **kwargs):

print func.__name__? ? ? # 輸出 'with_logging'

print func.__doc__? ? ? # 輸出 None

return func(*args, **kwargs)

return with_logging

# 函數(shù)

@logged

def f(x):

"""does some math"""

return x + x * x

logged(f)

不難發(fā)現(xiàn)猫态,函數(shù) f 被with_logging取代了佣蓉,當(dāng)然它的docstring,__name__就是變成了with_logging函數(shù)的信息了亲雪。好在我們有functools.wraps勇凭,wraps本身也是一個裝飾器,它能把原函數(shù)的元信息拷貝到裝飾器里面的 func 函數(shù)中义辕,這使得裝飾器里面的 func 函數(shù)也有和原函數(shù) foo 一樣的元信息了虾标。

from functools import wraps

def logged(func):

@wraps(func)

def with_logging(*args, **kwargs):

print func.__name__? ? ? # 輸出 'f'

print func.__doc__? ? ? # 輸出 'does some math'

return func(*args, **kwargs)

return with_logging

@logged

def f(x):

"""does some math"""

return x + x * x

裝飾器順序

一個函數(shù)還可以同時定義多個裝飾器,比如:

@a

@b

@c

def f ():

pass

它的執(zhí)行順序是從里到外灌砖,最先調(diào)用最里層的裝飾器璧函,最后調(diào)用最外層的裝飾器,它等效于

f = a(b(c(f)))

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末基显,一起剝皮案震驚了整個濱河市蘸吓,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌续镇,老刑警劉巖美澳,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異摸航,居然都是意外死亡制跟,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進店門酱虎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來雨膨,“玉大人,你說我怎么就攤上這事读串×募牵” “怎么了撒妈?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長排监。 經(jīng)常有香客問我狰右,道長,這世上最難降的妖魔是什么舆床? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任棋蚌,我火速辦了婚禮,結(jié)果婚禮上挨队,老公的妹妹穿的比我還像新娘谷暮。我一直安慰自己,他們只是感情好盛垦,可當(dāng)我...
    茶點故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布湿弦。 她就那樣靜靜地躺著,像睡著了一般腾夯。 火紅的嫁衣襯著肌膚如雪颊埃。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天俯在,我揣著相機與錄音竟秫,去河邊找鬼。 笑死跷乐,一個胖子當(dāng)著我的面吹牛肥败,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播愕提,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼馒稍,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了浅侨?” 一聲冷哼從身側(cè)響起纽谒,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎如输,沒想到半個月后鼓黔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡不见,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年澳化,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片稳吮。...
    茶點故事閱讀 40,424評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡缎谷,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出灶似,到底是詐尸還是另有隱情列林,我是刑警寧澤瑞你,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布,位于F島的核電站希痴,受9級特大地震影響者甲,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜润梯,卻給世界環(huán)境...
    茶點故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一过牙、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧纺铭,春花似錦、人聲如沸刀疙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽谦秧。三九已至竟纳,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間疚鲤,已是汗流浹背锥累。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留集歇,地道東北人桶略。 一個月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像诲宇,于是被迫代替她去往敵國和親际歼。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,435評論 2 359

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

  • 一. 有時候我們會有這樣需求: 在原有的邏輯前后添加一段邏輯 如: 在增/刪/改操作之前檢查用戶是否登錄姑蓝、某個操...
    元亨利貞o閱讀 697評論 1 4
  • Python裝飾器的高級用法(翻譯) 原文地址https://www.codementor.io/python/t...
    城南道閱讀 4,814評論 1 22
  • 以前在IMOOC上學(xué)習(xí)的筆記鹅心。今晚整理下發(fā)出來。 要理解裝飾器纺荧,先了解函數(shù)作用域和閉包旭愧。 函數(shù)作用域的查找順序概括...
    Ovie閱讀 364評論 0 2
  • www.yunxcloud.cn 首先要明白裝飾器是用來給函數(shù)增加額外功能的。 常用的工具函數(shù) import ti...
    彩色系閱讀 1,021評論 0 1