python裝飾器原理及應(yīng)用

在python編程中砰识,我們經(jīng)常看到下面的函數(shù)用法:

with open("test.txt", "w") as f:
f.write("hello world!")
習慣了java開發(fā)的python初學者佣渴,心里不免犯嘀咕:

文件open操作之后辫狼,為什么沒有close,不怕文件描述符資源耗盡嗎辛润?

文件write操作沒有異常捕獲膨处,不怕中斷程序主流程嗎?如果您也有同樣的憂慮,那太正常不過了真椿,起碼說明您是一位有“開發(fā)原則”的人鹃答,同時也說明您對其背后的原理了解存在盲區(qū)。如果是這種情況突硝,本文強烈建議您耐心閱讀完以下章節(jié)测摔。為了系統(tǒng)的闡述其背后的奧秘,本文從最基本的函數(shù)講起狞换。

關(guān)于函數(shù)
在Python中避咆,一切皆為對象舟肉,包括函數(shù)修噪。

def foo(num):
return num + 1

value = foo(3)
print(value)

def bar():
print("bar")

foo = bar
foo()
上面簡單的函數(shù)例子中,可以總結(jié)幾點信息:

函數(shù)名字foo可以作為變量名字路媚,指向函數(shù)對象

函數(shù)名字foo作為對象黄琼,可以賦值給變量value

函數(shù)名字foo可以作為變量名字,指向其他函數(shù)bar

函數(shù)名字(函數(shù)對象)通過括號調(diào)用函數(shù) 不僅如此整慎,作為對象的函數(shù)也具有一般對象的特性脏款,比如:

函數(shù)作為參數(shù)

def foo(num):
return num + 1

def bar(fun):
return fun(3)

value = bar(foo)
print(value)
函數(shù)作為返回值

def foo():
return 1

def bar():
return foo #注意這里沒有括號

print(bar()) # <function foo at 0x10a2f4140>

print(bar()()) # 1

等價于

print(foo()) # 1
函數(shù)嵌套

def outer():
x = 1
def inner():
print(x)
inner() # 注意這里有括號,直接被調(diào)用

outer() #
閉包

def outer(x):
def inner():
print(x)

return inner #沒括號裤园,不被直接調(diào)用

closure = outer(1) # closure就是一個閉包
closure()
同樣是嵌套函數(shù)撤师,只是稍改動一下,把局部變量 x 作為參數(shù)了傳遞進來拧揽,嵌套函數(shù)不再直接在函數(shù)里被調(diào)用剃盾,而是作為返回值返回,這里的 closure就是一個閉包淤袜,本質(zhì)上它還是函數(shù)痒谴,閉包是引用了自由變量(x)的函數(shù)(inner)。

裝飾器

def outer(func):
def inner():
print("before call fun")
func()
print("after call fun")
return inner

def foo():
print("foo")

new_foo = outer(foo)
new_foo()
outer 函數(shù)其實就是一個裝飾器:一個帶有函數(shù)作為參數(shù)并返回一個新函數(shù)的閉包.本質(zhì)上裝飾器也是函數(shù),outer 函數(shù)的返回值是 inner 函數(shù)铡羡。

注:上面示例中的裝飾器函數(shù)調(diào)用积蔚,可以用語法糖@簡寫為:

@outer
def foo():
print("foo")

foo()
我們進一步抽象裝飾器:

def decorator(func):
def wrapper(*args, **kw):
return func()
return wrapper

@decorator
def function():
print("hello, decorator")
可見,通過裝飾器烦周,可以讓代碼更加簡練尽爆、優(yōu)雅、可讀性更強读慎。

裝飾器進階
類裝飾器 基于類裝飾器的實現(xiàn)漱贱,必須實現(xiàn) call 和init 兩個內(nèi)置函數(shù)。 init :接收被裝飾函數(shù) call:實現(xiàn)裝飾邏輯贪壳。以日志打印為例:

class logger(object):
def init(self, func):
self.func = func

def __call__(self, *args, **kwargs):
    print("[INFO]: the function {func}() is running..."\
        .format(func=self.func.__name__))
    return self.func(*args, **kwargs)

@logger
def say(something):
print("say {}!".format(something))

say("hello")
裝飾類的裝飾器 裝飾器不僅可以裝飾函數(shù)饱亿,還可以裝飾類,比如如果想改寫類的方法的部分實現(xiàn),除了通過類繼承重載彪笼,還可以通過裝飾器钻注,實現(xiàn)如下:

def log_getattribute(cls):
# Get the original implementation
orig_getattribute = cls.getattribute

# Make a new definition
def new_getattribute(self, name):
    print('getting:', name)
    return orig_getattribute(self, name)

# Attach to the class and return
cls.__getattribute__ = new_getattribute
return cls

Example use

@log_getattribute
class A:
def init(self,x):
self.x = x
def spam(self):
pass

a = A(42)
print(a.x)
示例中,通過裝飾器函數(shù)log_getattribute修改原有類的屬性方法getattribute的指向來達到目的:通過指向新的方法new_getattribute配猫,在新的方法中在調(diào)用原來方法之前幅恋,添加額外邏輯。

偏函數(shù) 使用裝飾器的前提是裝飾器必須是可被調(diào)用的對象泵肄,比如函數(shù)捆交、實現(xiàn)了call 函數(shù)的類等,即將介紹的偏函數(shù)其實也是 callable 對象腐巢。在了解偏函數(shù)之前品追,先舉個例子:計算 100 加任意個數(shù)字的和。我們用parital函數(shù)解決這個問題:

from functools import partial

def add(*args):
return sum(args)

add_100 = partial(add, 100)
print(add_100(1, 2)) # 103

print(add_100(1, 2, 3)) # 106
跟上面的例子那樣冯丙,偏函數(shù)作用和裝飾器一樣肉瓦,它可以擴展函數(shù)的功能,但又不完全等價于裝飾器胃惜。通常應(yīng)用的場景是當我們要頻繁調(diào)用某個函數(shù)時泞莉,其中某些參數(shù)是已知的固定值,可以將這些固定值“固定”船殉,然后用其他的參數(shù)參與調(diào)用鲫趁。類似偏導數(shù)計算那樣,固定幾個變量利虫,對剩下的變量求導挨厚。我們看下partial的函數(shù)參數(shù)定義:

func = functools.partial(func, *args, **keywords)
func: 需要被擴展的函數(shù),返回的函數(shù)其實是一個類 func 的函數(shù)
*args: 需要被固定的位置參數(shù)
**kwargs: 需要被固定的關(guān)鍵字參數(shù)

如果在原來的函數(shù) func 中關(guān)鍵字不存在列吼,將會擴展幽崩,如果存在,則會覆蓋

同樣是剛剛求和的代碼寞钥,不同的是加入的關(guān)鍵字參數(shù)

def add(*args, *kwargs):
# 打印位置參數(shù)
for n in args:
print(n)
print("-"
20)
# 打印關(guān)鍵字參數(shù)
for k, v in kwargs.items():
print('%s:%s' % (k, v))
# 暫不做返回慌申,只看下參數(shù)效果,理解 partial 用法

普通調(diào)用

add(1, 2, 3, v1=10, v2=20)
add_partial = partial(add, 10, k1=10, k2=20)
add_partial(1, 2, 3, k3=20)
偏函數(shù)與裝飾器 我們再看看如何使用類和偏函數(shù)結(jié)合實現(xiàn)裝飾器理郑,如下所示蹄溉,DelayFunc 是一個實現(xiàn)了call 的類,delay 返回一個偏函數(shù)您炉,在這里 delay 就可以做為一個裝飾器:

import time
import functools

class DelayFunc:
def init(self, duration, func):
self.duration = duration
self.func = func

def __call__(self, *args, **kwargs):
    print(f'Wait for {self.duration} seconds...')
    time.sleep(self.duration)
    return self.func(*args, **kwargs)

def delay(duration):
"""
裝飾器:推遲某個函數(shù)的執(zhí)行柒爵。
"""
# 此處為了避免定義額外函數(shù),
# 直接使用 functools.partial 幫助構(gòu)造 DelayFunc 實例
return functools.partial(DelayFunc, duration)

@delay(duration=2)
def add(a, b):
return a+b

wraps
繼續(xù)深入函數(shù)裝飾器赚爵,首先打印被裝飾的函數(shù)function的名字:

def decorator(func):
def wrapper(*args, **kw):
return func()
return wrapper

@decorator
def function():
print("hello, decorator")

print(function.name) #wrapper
輸出發(fā)現(xiàn)是wrapper棉胀,其實這也好理解法瑟,因為decorator返回的就是wrapper。但有時我們需要返回function的本來名字唁奢,那怎么做呢霎挟?python 的functools模塊提供了一系列的高階函數(shù)以及對可調(diào)用對象的操作,比如reduce麻掸,partial酥夭,wraps等。其中partial作為偏函數(shù)脊奋,在前面已經(jīng)介紹過熬北,warps旨在消除裝飾器對原函數(shù)造成的影響,即對原函數(shù)的相關(guān)屬性(比如name)進行拷貝诚隙,以達到裝飾器不修改原函數(shù)(屬性)的目的:

from functools import wraps

def decorator(func):
@wraps(func)
def wrapper(*args, **kw):
print(func.name)
return func()
return wrapper

@decorator
def function():
print("hello, decorator")

function()
print(function.name)
注意代碼中return func()讶隐,括號表示調(diào)用執(zhí)行函數(shù)。作為對比最楷,請看下面的調(diào)用:

from functools import wraps

def decorator(func):
@wraps(func)
def wrapper(*args, **kw):
print(func.name)
return func
return wrapper

@decorator
def function():
print("hello, decorator")

因為裝飾返回func整份,不會發(fā)生調(diào)用,因此需要兩對括號籽孙,其中function()返回的是函數(shù)定義。

print(function())
function()()
print(function.name)
裝飾器應(yīng)用之contextmanager
contextmanager是python中一個使用廣泛的上下文管理器火俄,(實際上也是裝飾器)經(jīng)常跟with語句一起使用犯建,用于精確地控制資源的分配和釋放」峡停回憶以下常規(guī)代碼結(jié)構(gòu):

def controlled_execution(callback):
try:
#比如環(huán)境初始化适瓦、資源分配等
set things up
callback(thing)
finally:
#比如資源回收、事物提交等
tear things down

def my_function(thing):
#執(zhí)行具體的業(yè)務(wù)邏輯
do something

controlled_execution(my_function)
以上為了防止業(yè)務(wù)邏輯出現(xiàn)異常谱仪,導致一些必須要執(zhí)行的操作無法執(zhí)行玻熙,通常使用try...finally語句,保證必要操作一定被執(zhí)行疯攒。但是如果代碼中大量使用這種語句嗦随,又導致程序邏輯冗余,可讀性變差敬尺。但是結(jié)合with枚尼,并將以上語句稍作改動:將try...finally的邏輯拆分成兩個函數(shù),分別執(zhí)行比如資源的初始化和釋放砂吞,封裝在一個class中:

class controlled_execution:
def enter(self):
set things up
return thing
def exit(self, type, value, traceback):
tear things down

with controlled_execution() as thing:
# code body
do something
其中with expression [as variable],用來簡化 try / finally 語句署恍。當執(zhí)行with語句、進入代碼塊前蜻直,調(diào)用enter方法盯质,代碼塊執(zhí)行結(jié)束之后執(zhí)行exit方法袁串。需要注意的是可以根據(jù)exit方法的返回值來決定是否拋出異常,如果沒有返回值或者返回值為 False 呼巷,則異常由上下文管理器處理般婆,如果為 True 則由用戶自己處理。上述代碼可以通過contextmanager進一步簡化:

@contextmanager
def controlled_execution():
#set things up
yield thing
#tear things down

with controlled_execution() as t:
print(t)
引入yield將函數(shù)變成生成器朵逝,yield將函數(shù)體分為兩部分:yield之前的語句在執(zhí)行with代碼塊之前執(zhí)行蔚袍,yield之后的代碼塊在with代碼塊之后執(zhí)行。到此為止配名,相信大家能夠理解文章開篇提到的代碼塊了啤咽,然后基于此,我們也可以自定義一個open函數(shù):

from contextlib import contextmanager

@contextmanager
def my_open(name):
f = open(name, 'w')
yield f
f.close()

with my_open('some_file') as f:
f.write('hola!')

參考

https://mp.weixin.qq.com/s/cMqJulHjfo5oYfnwKDP7zw

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末渠脉,一起剝皮案震驚了整個濱河市宇整,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌芋膘,老刑警劉巖鳞青,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異为朋,居然都是意外死亡臂拓,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進店門习寸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來胶惰,“玉大人,你說我怎么就攤上這事霞溪》踔停” “怎么了?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵鸯匹,是天一觀的道長坊饶。 經(jīng)常有香客問我,道長殴蓬,這世上最難降的妖魔是什么匿级? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮科雳,結(jié)果婚禮上根蟹,老公的妹妹穿的比我還像新娘。我一直安慰自己糟秘,他們只是感情好简逮,可當我...
    茶點故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著尿赚,像睡著了一般散庶。 火紅的嫁衣襯著肌膚如雪蕉堰。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天悲龟,我揣著相機與錄音屋讶,去河邊找鬼。 笑死须教,一個胖子當著我的面吹牛皿渗,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播轻腺,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼乐疆,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了贬养?” 一聲冷哼從身側(cè)響起挤土,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎误算,沒想到半個月后仰美,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡儿礼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年咖杂,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蜘犁。...
    茶點故事閱讀 38,654評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡翰苫,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出这橙,到底是詐尸還是另有隱情,我是刑警寧澤导披,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布屈扎,位于F島的核電站,受9級特大地震影響撩匕,放射性物質(zhì)發(fā)生泄漏鹰晨。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一止毕、第九天 我趴在偏房一處隱蔽的房頂上張望模蜡。 院中可真熱鬧,春花似錦扁凛、人聲如沸忍疾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽卤妒。三九已至甥绿,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間则披,已是汗流浹背共缕。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留士复,地道東北人图谷。 一個月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像阱洪,于是被迫代替她去往敵國和親便贵。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,543評論 2 349

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

  • 裝飾器本質(zhì)上是一個函數(shù)澄峰,該函數(shù)用來處理其他函數(shù)嫉沽,它可以讓其他函數(shù)在不需要修改代碼的前提下增加額外的功能,裝飾器的返...
    胡一巴閱讀 406評論 0 0
  • 每個人都有的內(nèi)褲主要功能是用來遮羞俏竞,但是到了冬天它沒法為我們防風御寒绸硕,咋辦?我們想到的一個辦法就是把內(nèi)褲改造一下魂毁,...
    chen_000閱讀 1,360評論 0 3
  • 一玻佩、裝飾器的基本使用 在不改變函數(shù)源代碼的前提下,給函數(shù)添加新的功能席楚,這時就需要用到“裝飾器”巍扛。 0.開放封閉原則...
    NJingZYuan閱讀 523評論 0 0
  • 一. 有時候我們會有這樣需求: 在原有的邏輯前后添加一段邏輯 如: 在增/刪/改操作之前檢查用戶是否登錄粱锐、某個操...
    元亨利貞o閱讀 695評論 1 4
  • 夸獎寶寶,就像使用抗生素一樣,需要有一定的標準蒲肋,如果夸獎不到位,對孩子也會產(chǎn)生負面影響侍瑟。那么夸獎寶寶要遵循怎樣的原...
    光谷魚兒也想飛閱讀 109評論 0 0