閉包和裝飾器
1.8 閉包和裝飾器
學習目標
? 1. 能夠說出閉包的定義形式
? 2. 能夠說出裝飾器的實現(xiàn)形式
? 3. 能夠說出裝飾器的作用
? 4. 能夠說出裝飾器的不同形式
? 5. 能夠說出萬能裝飾器的實現(xiàn)形式
? 6. 能夠說出裝飾器的執(zhí)行過程
--------------------------------------------------------------------------------
1.8.1 閉包和裝飾器概述
? 什么是閉包:
? ? ? 閉包是指在一個函數(shù)中定義了一個另外一個函數(shù)本谜,內(nèi)函數(shù)里運用了外函數(shù)的臨時變量(實際參數(shù)也是臨時變量),并且外函數(shù)的返回值是內(nèi)函數(shù)的引用(一切皆引用,所有的函數(shù)名字都只是函數(shù)體在內(nèi)存空間的一個引用蒿赢。)
? 閉包的作用:
? ? ? 可以隱藏內(nèi)部函數(shù)的工作細節(jié)碰缔,只給外部使用者提供一個可以執(zhí)行的內(nèi)部函數(shù)的引用韭脊。
? ? ? 避免了使用全局變量什乙,保證了程序的封裝性
? ? ? 保證了內(nèi)函數(shù)的安全性淋硝,其他函數(shù)不能訪問
? 什么是裝飾器:
? ? ? 裝飾器就是用于拓展已有函數(shù)功能的一種函數(shù)制恍,這個函數(shù)的特殊之處在于它的返回值也是一個函數(shù)父能,實際上就是利用閉包語法實現(xiàn)的。
? 裝飾器的作用
? ? ? 在不用更改原函數(shù)的代碼前提下給函數(shù)增加新的功能净神。
1.8.2 思考
有一種叫做五步棋的游戲何吝,在一個 五行五列的網(wǎng)格上雙方各持一色棋子,在棋子移動時鹃唯,只能以橫向或縱向的形式移動爱榕。
如果實現(xiàn)游戲,如何能記錄當前棋子移動的位置(也就是坐標)呢坡慌?
1.8.3 技術(shù)點回顧
在使用函數(shù)時黔酥,函數(shù)可以傳遞參數(shù),函數(shù)也可以返回數(shù)據(jù)洪橘。
在傳遞和返回數(shù)據(jù)時跪者,一般是傳遞返回的固定數(shù)據(jù)和代碼執(zhí)行的結(jié)果。
在Pyhton中熄求,函數(shù)也是一個對象渣玲,在函數(shù)操作中,函數(shù)對象也可以當成一個參數(shù)或一個返回值進行返回弟晚。
當程序內(nèi)或程序外拿到參數(shù)的引用后就可以直接使用這個函數(shù)忘衍,(原理回想下深淺拷貝中的賦值)
def show():
? ? print('show run')
show()
func = show
func()
程序執(zhí)行結(jié)果:
show run
show run
1.8.4 閉包
? 閉包就是在一個外部函數(shù)中定義了一個內(nèi)部函數(shù)逾苫,并且在內(nèi)部函數(shù)中使用了外部函數(shù)的變量,并返回了內(nèi)部函數(shù)的引用枚钓。
? nonlocal 的使用
? ? ? nonlocal 變量名 ——》聲明變量為非本地變量
? ? ? 如果在閉包的內(nèi)部函數(shù)中直接使用外部函數(shù)的變量時铅搓,不需要任何操作,直接使用就可以了秘噪。
? ? ? 但是如果要修改外部變量的值狸吞,需要將變量聲明為 nonlocal,那么建議將 nonlocal 寫在內(nèi)部函數(shù)的第一行指煎。
利用函數(shù)可以被傳遞和返回的特性蹋偏,在開發(fā)過程中,可以隱藏更多的實現(xiàn)細節(jié)至壤。
n = 1? # 全局變量
def show(): # 公有函數(shù)
? ? print('show: ',n)
def callFunc(func): #公有函數(shù)
? ? return func
s = callFunc(show)? # 函數(shù)執(zhí)行
s()
show()
在這段代碼中威始,在實際開發(fā)中并沒有實際意義,只是簡單示意了函數(shù)可以被當做參數(shù)和返回值使用像街。
但是這段代碼并不完美
第一黎棠,盡量不要使用全局變量,因為全局變量會破壞程序的封裝性镰绎。
第二脓斩,如果 show 函數(shù)不想被 callFunc 以外的函數(shù)進行訪問時,是無法控制的畴栖。
所以可以改進如下:
def callFunc():
? ? n = 1
? ? def show():
? ? ? ? print('show: ', n)
? ? return show
s = callFunc()
s()
# show() 因為 show 函數(shù)定義在 callFunc 內(nèi)部随静,所以外部不可見,不能使用
代碼改進后吗讶,去掉了全局變量的使用燎猛。而且將 show 函數(shù)封裝在了 callFunc 函數(shù)內(nèi)部,使外部不可見照皆,不能使用 show 函數(shù)重绷,隱藏了實現(xiàn)細節(jié)
程序在執(zhí)行時,callFunc 函數(shù)返回了內(nèi)部定義的 show 函數(shù)膜毁,并且 在 show 函數(shù)內(nèi)部使用了外部函數(shù)的變量昭卓。
在 show 函數(shù)返回時,保存了當前的執(zhí)行環(huán)境爽茴,也就是會在 show 函數(shù)中使用的外部變量 n 葬凳。
因為 n 是一個 callFunc 函數(shù)中的局部變量,正常情況下 callFunc 函數(shù)執(zhí)行結(jié)束后室奏,n 就會被釋放。
但是現(xiàn)在因為 callFunc 函數(shù)中返回了 show 函數(shù)劲装,show 函數(shù)在外部還會再執(zhí)行胧沫,所以程序會將 show 函數(shù)所需的執(zhí)行環(huán)境保存下來昌简。
這種形式就是閉包。
? 利用閉包完成棋子的移動
'''閉包實現(xiàn)棋子移動'''
# 定義一個外部函數(shù)
def outer():
? ? # 在外部函數(shù)中定義一個保存坐標的列表
? ? position = [0,0]
? ? # 定義一個內(nèi)部函數(shù)绒怨,參數(shù)為移動方式和步長
? ? # 移動方式為列表 [x,y] x,y分別只能取 -1纯赎,0,1三個值南蹂,表示反向犬金,不動,正向
? ? def inner(direction,step):
? ? ? ? # 計算坐標值
? ? ? ? position[0] = position[0] + direction[0] * step
? ? ? ? position[1] = position[1] + direction[1] * step
? ? ? ? # 返回移動后的坐標
? ? ? ? return position
? ? # 返回內(nèi)部函數(shù)
? ? return inner
# 獲取內(nèi)部函數(shù)
move = outer()
# 移動
print(move([1, 0], 10))
print(move([0, 1], 10))
print(move([-1, 0], 10))
程序執(zhí)行結(jié)果:
[10, 0]
[10, 10]
[0, 10]
nonlocal 的使用 如果在閉包的內(nèi)部函數(shù)中直接使用外部函數(shù)的變量時六剥,不需要任何操作晚顷,直接使用就可以了。
但是如果要修改外部變量的值疗疟,需要將變量聲明為 nonlocal
def callFunc():
? ? m = 1
? ? n = 2
? ? def show():
? ? ? ? print('show - m: ', m)
? ? ? ? nonlocal n #如果不加會報錯该默。
? ? ? ? n *= 10
? ? ? ? print('show - n: ', n)
? ? return show
s = callFunc()
s()
nonlocal 聲明變量為非本地變量,如果確定在程序要修改外部變量策彤,那么建議將 nonlocal 寫在內(nèi)部函數(shù)的第一行栓袖。
小結(jié): 閉包就是在一個外部函數(shù)中定義了一個內(nèi)部函數(shù),并且在內(nèi)部函數(shù)中使用了外部函數(shù)的變量店诗,并返回了內(nèi)部函數(shù)裹刮。
1.8.5 裝飾器
? 裝飾器的定義:
? ? ? 不改變原有函數(shù)功能的基礎上,對函數(shù)進行擴展的形式庞瘸,稱為裝飾器捧弃。
? 裝飾器的本質(zhì):
? ? ? 實際上就是一個以閉包的形式定義的函數(shù) 。
? 裝飾器的作用:
? ? ? 為現(xiàn)有存在的函數(shù)恕洲,在不改變函數(shù)的基礎上去增加一些功能進行裝飾塔橡。
? 裝飾器函數(shù)的使用:
? ? ? 在被裝飾的函數(shù)的前一行,使用 @xxx (@裝飾器(閉包)函數(shù)名) 形式來裝飾
? 裝飾器的好處:
? ? ? 定義好了裝飾器(閉包)函數(shù)后霜第,只需要通過 @xxx (@裝飾器(閉包)函數(shù)名)形式的裝飾器語法葛家,將 @xxx (@裝飾器(閉包)函數(shù)名) 加到要裝飾的函數(shù)前即可。
? 裝飾器的原理:
? ? ? 在執(zhí)行 @xxx 時 泌类,實際就是將 原函數(shù)傳遞到閉包函數(shù)中癞谒,然后原函數(shù)的引用指向閉包返回的裝飾過的內(nèi)部函數(shù)的引用。
? ? ? @count_time? ? # 這實際就相當于解決方法3中的 my_count = count_tiem(my_count)(把被裝飾的函數(shù)引用當作參數(shù)傳入到裝飾器函數(shù)刃榨,被裝飾的函數(shù)引用指向裝飾器函數(shù)返回的引用)
實例應用
現(xiàn)在一個項目中弹砚,有很多函數(shù) ,由于項目越來越大枢希,功能越來越多桌吃,導致程序越來越慢。
其中一個功能函數(shù)功能苞轿,實現(xiàn)一百萬次的累加茅诱。
def my_count():
? ? s = 0
? ? for i in range(1000001):
? ? ? ? s += i
? ? print('sum : ', s)
現(xiàn)在想計算一下函數(shù)的運行時間逗物,如何解決?如何能應用到所有函數(shù)上瑟俭?
解決辦法 1
start = time.time()
my_count()
end = time.time()
print('共計執(zhí)行:%s 秒'%(end - start)) # 使用%d顯示翎卓,取整后是0秒,因為不到一秒
這種辦法是最簡單的實現(xiàn)方式摆寄,但是一個函數(shù)沒問題失暴,但是要有1000個函數(shù),那么每個函數(shù)都要寫一遍微饥,非常麻煩并且代碼量憑空多了三千行逗扒。
這明顯是不符合開發(fā)的原則的,代碼太冗余
解決辦法 2
def count_time(func):
? ? start = time.time()
? ? func()
? ? end = time.time()
? ? print('共計執(zhí)行:%s 秒'%(end - start)) # 使用%d顯示畜号,取整后是0秒缴阎,因為不到一秒
count_time(my_count)
經(jīng)過修改后,定了一個函數(shù)來實現(xiàn)計算時間的功能简软,通過傳參蛮拔,將需要計算的函數(shù)傳遞進去,進行計算痹升。
修改后的代碼建炫,比之前好很多。
但是在使用時疼蛾,還是需要將函數(shù)傳入到時間計算函數(shù)中肛跌。
能不能實現(xiàn)在使用時,不影響函數(shù)原來的使用方式察郁,而又能實現(xiàn)計算功能呢衍慎?
解決辦法 3
def count_time(func):
? ? def wrapper():? ? ? #wrapper 裝飾
? ? ? ? start = time.time()
? ? ? ? func()
? ? ? ? end = time.time()
? ? ? ? print('共計執(zhí)行:%s 秒'%(end - start)) # 使用%d顯示,取整后是0秒皮钠,因為不到一秒
? ? return wrapper
my_count = count_time(my_count)
my_count()
此次在解釋辦法2的基礎上稳捆,又將功能外添加了一層函數(shù)定義,實現(xiàn)了以閉包的形式來進行定義
在使用時麦轰,讓 my_count 函數(shù)重新指向了 count_time 函數(shù)返回后的函數(shù)引用乔夯。這樣在使用 my_count 函數(shù)時,就和原來使用方式一樣了款侵。
這種形式實際上就是塌裝飾器的實現(xiàn)原理末荐。
之前我們用過裝飾器,如:@property 等
那么是否可以像系統(tǒng)裝飾器一樣改進呢新锈?
解決辦法 4
import time
def count_time(func):
? ? def wrapper():? ? ? #wrapper 裝飾
? ? ? ? start = time.time()
? ? ? ? func()
? ? ? ? end = time.time()
? ? ? ? print('共計執(zhí)行:%s 秒'%(end - start)) # 使用%d顯示甲脏,取整后是0秒,因為不到一秒
? ? return wrapper
@count_time? ? # 這實際就相當于解決方法3中的 my_count = count_tiem(my_count)
def my_count():
? ? s = 0
? ? for i in range(10000001):
? ? ? ? s += i
? ? print('sum : ', s)
my_count()
這樣實現(xiàn)的好處是,定義好了閉包函數(shù)后剃幌。只需要通過 @xxx 形式的裝飾器語法聋涨,將 @xxx 加到要裝飾的函數(shù)前即可晾浴。
使用者在使用時负乡,根本不需要知道被裝飾了。只需要知道原來的函數(shù)功能是什么即可脊凰。
這種不改變原有函數(shù)功能基礎上抖棘,對函數(shù)進行擴展的形式,稱為裝飾器狸涌。
在執(zhí)行 @xxx 時 切省,實際就是將 原函數(shù)傳遞到閉包中,然后原函數(shù)的引用指向閉包返回的裝飾過的內(nèi)部函數(shù)的引用帕胆。
1.8.6 裝飾器的幾種形式
根據(jù)被裝飾函數(shù)定義的參數(shù)和返回值定義形式不同朝捆,裝飾器也對應幾種變形。
? 無參無返回值
? ? def setFunc(func):
? ? ? ? def wrapper():
? ? ? ? ? ? print('Start')
? ? ? ? ? ? func()
? ? ? ? ? ? print('End')
? ? ? ? return wrapper
? ? @setFunc
? ? def show():
? ? ? ? print('show')
? ? show()
? 無參有返回值
? ? def setFunc(func):
? ? ? ? def wrapper():
? ? ? ? ? ? print('Start')
? ? ? ? ? ? return func()
? ? ? ? return wrapper
? ? @setFunc? # show = setFunc(show)
? ? def show():
? ? ? ? return 100
? ? print(show() * 100)
? 有參無返回值
? ? def setFunc(func):
? ? ? ? def wrapper(s):
? ? ? ? ? ? print('Start')
? ? ? ? ? ? func(s)
? ? ? ? ? ? print('End')
? ? ? ? return wrapper
? ? @setFunc?
? ? def show(s):
? ? ? ? print('Hello %s' % s)
? ? show('Tom')
? 有參有返回值
? ? def setFunc(func):
? ? ? ? def wrapper(x, y):
? ? ? ? ? ? print('Start')
? ? ? ? ? ? return func(x, y)
? ? ? ? return? wrapper
? ? @setFunc
? ? def myAdd(x, y):
? ? ? ? return? x + y
? ? print(myAdd(1, 2))
1.8.7 萬能裝飾器
萬能裝飾器的定義:
通過可變參數(shù)和關鍵字參數(shù)來接收不同的參數(shù)類型定義出來的裝飾器函數(shù)適用于任何形式的函數(shù)懒豹。
? def setFunc(func):
? ? ? ? def wrapper(*args, **kwargs):? # 接收不同的參數(shù)
? ? ? ? ? ? print('wrapper context')
? ? ? ? ? ? return func(*args, *kwargs) # 再原樣傳回給被裝飾的函數(shù)
? ? ? ? return wrapper
? ? @setFunc
? ? def show(name, age):
? ? ? ? print(name,age)
? ? show('tom',12)
1.8.8 類實現(xiàn)裝飾形式
通過類的定義實現(xiàn)裝飾器形式:
? 在類中通過使用 __init__ 和 __call__方法來實現(xiàn)
? 通過重寫__init__初始化方法芙盘,接收參數(shù),將要被裝飾的函數(shù)傳進來并記錄下來(相當于外函數(shù)接收參數(shù))
? 通過重寫 __call__ 方法來實現(xiàn)裝飾內(nèi)容(相當于內(nèi)函數(shù)實現(xiàn)裝飾內(nèi)容)
? @Test? ——》 show = Test(show) show由原來引用函數(shù)脸秽,裝飾后變成 引用Test裝飾類的對象
? show() ——》實際上是仿函數(shù)(是在實現(xiàn)__call__魔法方法后,將對象當做函數(shù)一樣去使用)儒老,即對象調(diào)用方法實現(xiàn)了裝飾器
? ? class Test(object):
? ? ? ? # 通過初始化方法,將要被裝飾的函數(shù)傳進來并記錄下來
? ? ? ? def __init__(self, func):
? ? ? ? ? ? self.__func = func
? ? ? ? # 重寫 __call__ 方法來實現(xiàn)裝飾內(nèi)容
? ? ? ? def __call__(self, *args, **kwargs):
? ? ? ? ? ? print('wrapper context')
? ? ? ? ? ? self.__func(*args, **kwargs)
? ? # 實際通過類的魔法方法call來實現(xiàn)
? ? @Test? # --> show = Test(show) show由原來引用函數(shù),裝飾后變成引用Test裝飾類的對象
? ? def show():
? ? ? ? pass
? ? show()? # 對象調(diào)用方法,實際上是調(diào)用魔法方法call,實現(xiàn)了裝飾器
1.8.9 函數(shù)被多個裝飾器所裝飾(了解)
一個函數(shù)在使用時记餐,通過一個裝飾器來擴展驮樊,可能并不能完成達到預期。
Python 中允許一個裝飾器裝飾多個函數(shù)和一個函數(shù)被多個裝飾器所裝飾片酝。
? 多個裝飾器的裝飾過程:
? ? ? 從下向上裝飾(即從里往外執(zhí)行)囚衔,先裝飾函數(shù),然后再向外(向上)一層一層裝飾雕沿。
# 裝飾器1
def setFunc1(func):
? ? def wrapper1(*args, **kwargs):
? ? ? ? print('Wrapper Context 1 Start...')
? ? ? ? func(args, kwargs)
? ? ? ? print('Wrapper Context 1 End...')
? ? return wrapper
# 裝飾器2
def setFunc2(func):
? ? def wrapper2(*args, **kwargs):
? ? ? ? print('Wrapper Context 2 Start...')
? ? ? ? func(args, kwargs)
? ? ? ? print('Wrapper Context 2 End...')
? ? return wrapper
#一個函數(shù)被裝飾了兩次
@setFunc1
@setFunc2
def show(*args, **kwargs):
? ? print('Show Run ...')
show()
程序執(zhí)行結(jié)果 :
Wrapper Context 1 Start...
Wrapper Context 2 Start...
Show Run ...
Wrapper Context 2 End...
Wrapper Context 1 End...
這個裝飾器的裝飾過程是
從下向上裝飾练湿,即從里往外執(zhí)行,先裝飾函數(shù)晦炊,然后再一層一層裝飾鞠鲜。
@setFunc2 -> show = setFunc2(show) -> show = setFunc2.wrapper2 @setFunc1 -> show = setFunc1(setFunc2.wrapper2) -> show = setFunc1.wrapper1(setFunc2.wrapper2(show))
1.8.10 裝飾器傳參:
裝飾器在使用過程中,可能需要對裝飾器進行傳參
? 在定義可以傳參的裝飾器閉包時断国,需要定義三層函數(shù)
? 最外層函數(shù)用來接收裝飾器的參數(shù)
? 中間層用來實現(xiàn)裝飾器
? 最內(nèi)層用來執(zhí)行具體的裝飾內(nèi)容
? 無論有幾層或者幾個裝飾器去裝飾已有函數(shù)贤姆,最終函數(shù)都是引用裝飾器的最內(nèi)層的函數(shù)。
? @xxx(xxx)? 先執(zhí)行傳參 xxx(xxx) 稳衬,實際就是執(zhí)行函數(shù)調(diào)用霞捡,得到中間層函數(shù), 與@組合后變成裝飾器形式薄疚,再進行裝飾
# 定義一個路由字典
router = {}
# 實現(xiàn)一個裝飾器,讓這個裝飾器來實現(xiàn)自動將 url 和 功能函數(shù)的匹配關系存到路由字典中
# 接收url參數(shù)
def set_args(args):
? ? # 真正用來去裝飾接收的函數(shù)
? ? def set_func(func):
? ? ? ? # 真正裝飾函數(shù)
? ? ? ? def wrapper(*args, **kwargs):
? ? ? ? ? ? func()
? ? ? ? # 因為 wrapper 就是指向被裝飾的函數(shù)
? ? ? ? router[args] = wrapper
? ? ? ? return wrapper
? ? return set_func
# 功能函數(shù)
# @set_args('login.html')? ? ? -> @set_func
@set_args('login.html')
def login():
? ? print('Login Run ...')
@set_args('nba.html')
def nba():
? ? print('NBA Run ...')
@set_args('news.html')
def news():
? ? print('News Run ...')
@set_args('11.html')
def double_one():
? ? print('雙十一')
# 模擬的運行函數(shù)
def run(url):
? ? # 通過傳入?yún)?shù)碧信,也就是訪問地址赊琳,來到路由字典中去找到對應的功能函數(shù)
? ? func = router[url]
? ? # 執(zhí)行相應的功能函數(shù)
? ? func()
# 模擬請求
run('login.html')
run('nba.html')
run('news.html')
run('11.html')
print(router)
1.8.11 總結(jié):
? 1. 函數(shù)可以像普通變量一樣,做為函數(shù)的參數(shù)或返回值進行傳遞
? 2. 函數(shù)內(nèi)部可以定義另外一個函數(shù)砰碴,這樣做的目的可以隱藏函數(shù)功能的實現(xiàn)
? 3. 閉包實際也是一種函數(shù)定義形式躏筏。
? 4. 閉包定義規(guī)則是在外部函數(shù)中定義一個內(nèi)部函數(shù),內(nèi)部函數(shù)使用外部函數(shù)的變量呈枉,并返回內(nèi)部函數(shù)的引用
? 5. Python 中裝飾器就是由閉包來實現(xiàn)的
? 6. 裝飾器的作用是在不改變現(xiàn)有函數(shù)基礎上趁尼,為函數(shù)增加功能。
? 7. 通過在已有函數(shù)前猖辫,通過 @閉包函數(shù)名 的形式來給已有函數(shù)添加裝飾器
? 8. 裝飾器函數(shù)根據(jù)參數(shù)和返回值的不同酥泞,可細分為四種定義形式
? 9. 可以通過可變參數(shù)和關鍵字參數(shù)來實現(xiàn)能用裝飾器定義形式
? 10. 一個裝飾器可以為多個函數(shù)提供裝飾功能,只需要在被裝飾的函數(shù)前加 @xxx 即可
? 11. 通過類也可以實現(xiàn)裝飾器效果啃憎,需要重寫 __init__ 和 __call__ 函數(shù)
? 12. 類實現(xiàn)的裝飾器在裝飾函數(shù)后芝囤,原來的函數(shù)引用不在是函數(shù),而是裝飾類的對象
? 13. 一個函數(shù)也可以被多個裝飾器所裝飾辛萍,但是實際在使用時悯姊,并不多見,了解形式即可