閉包和裝飾器
1.8 閉包和裝飾器
學(xué)習(xí)目標(biāo)
? 1. 能夠說出閉包的定義形式
? 2. 能夠說出裝飾器的實(shí)現(xiàn)形式
? 3. 能夠說出裝飾器的作用
? 4. 能夠說出裝飾器的不同形式
? 5. 能夠說出萬(wàn)能裝飾器的實(shí)現(xiàn)形式
? 6. 能夠說出裝飾器的執(zhí)行過程
--------------------------------------------------------------------------------
1.8.1 閉包和裝飾器概述
? 什么是閉包:
? ? ? 閉包是指在一個(gè)函數(shù)中定義了一個(gè)另外一個(gè)函數(shù),內(nèi)函數(shù)里運(yùn)用了外函數(shù)的臨時(shí)變量(實(shí)際參數(shù)也是臨時(shí)變量),并且外函數(shù)的返回值是內(nèi)函數(shù)的引用(一切皆引用,所有的函數(shù)名字都只是函數(shù)體在內(nèi)存空間的一個(gè)引用。)
? 閉包的作用:
? ? ? 可以隱藏內(nèi)部函數(shù)的工作細(xì)節(jié),只給外部使用者提供一個(gè)可以執(zhí)行的內(nèi)部函數(shù)的引用。
? ? ? 避免了使用全局變量,保證了程序的封裝性
? ? ? 保證了內(nèi)函數(shù)的安全性,其他函數(shù)不能訪問
? 什么是裝飾器:
? ? ? 裝飾器就是用于拓展已有函數(shù)功能的一種函數(shù),這個(gè)函數(shù)的特殊之處在于它的返回值也是一個(gè)函數(shù),實(shí)際上就是利用閉包語(yǔ)法實(shí)現(xiàn)的。
? 裝飾器的作用
? ? ? 在不用更改原函數(shù)的代碼前提下給函數(shù)增加新的功能。
1.8.2 思考
有一種叫做五步棋的游戲,在一個(gè) 五行五列的網(wǎng)格上雙方各持一色棋子,在棋子移動(dòng)時(shí),只能以橫向或縱向的形式移動(dòng)。
如果實(shí)現(xiàn)游戲,如何能記錄當(dāng)前棋子移動(dòng)的位置(也就是坐標(biāo))呢?
1.8.3 技術(shù)點(diǎn)回顧
在使用函數(shù)時(shí),函數(shù)可以傳遞參數(shù),函數(shù)也可以返回?cái)?shù)據(jù)。
在傳遞和返回?cái)?shù)據(jù)時(shí),一般是傳遞返回的固定數(shù)據(jù)和代碼執(zhí)行的結(jié)果。
在Pyhton中,函數(shù)也是一個(gè)對(duì)象,在函數(shù)操作中,函數(shù)對(duì)象也可以當(dāng)成一個(gè)參數(shù)或一個(gè)返回值進(jìn)行返回。
當(dāng)程序內(nèi)或程序外拿到參數(shù)的引用后就可以直接使用這個(gè)函數(shù),(原理回想下深淺拷貝中的賦值)
def show():
? ? print('show run')
show()
func = show
func()
程序執(zhí)行結(jié)果:
show run
show run
1.8.4 閉包
? 閉包就是在一個(gè)外部函數(shù)中定義了一個(gè)內(nèi)部函數(shù),并且在內(nèi)部函數(shù)中使用了外部函數(shù)的變量,并返回了內(nèi)部函數(shù)的引用。
? nonlocal 的使用
? ? ? nonlocal 變量名 ——》聲明變量為非本地變量
? ? ? 如果在閉包的內(nèi)部函數(shù)中直接使用外部函數(shù)的變量時(shí),不需要任何操作,直接使用就可以了。
? ? ? 但是如果要修改外部變量的值,需要將變量聲明為 nonlocal,那么建議將 nonlocal 寫在內(nèi)部函數(shù)的第一行。
利用函數(shù)可以被傳遞和返回的特性,在開發(fā)過程中,可以隱藏更多的實(shí)現(xiàn)細(xì)節(jié)。
n = 1? # 全局變量
def show(): # 公有函數(shù)
? ? print('show: ',n)
def callFunc(func): #公有函數(shù)
? ? return func
s = callFunc(show)? # 函數(shù)執(zhí)行
s()
show()
在這段代碼中,在實(shí)際開發(fā)中并沒有實(shí)際意義,只是簡(jiǎn)單示意了函數(shù)可以被當(dāng)做參數(shù)和返回值使用。
但是這段代碼并不完美
第一,盡量不要使用全局變量,因?yàn)槿肿兞繒?huì)破壞程序的封裝性。
第二,如果 show 函數(shù)不想被 callFunc 以外的函數(shù)進(jìn)行訪問時(shí),是無法控制的。
所以可以改進(jìn)如下:
def callFunc():
? ? n = 1
? ? def show():
? ? ? ? print('show: ', n)
? ? return show
s = callFunc()
s()
# show() 因?yàn)?show 函數(shù)定義在 callFunc 內(nèi)部,所以外部不可見,不能使用
代碼改進(jìn)后,去掉了全局變量的使用。而且將 show 函數(shù)封裝在了 callFunc 函數(shù)內(nèi)部,使外部不可見,不能使用 show 函數(shù),隱藏了實(shí)現(xiàn)細(xì)節(jié)
程序在執(zhí)行時(shí),callFunc 函數(shù)返回了內(nèi)部定義的 show 函數(shù),并且 在 show 函數(shù)內(nèi)部使用了外部函數(shù)的變量。
在 show 函數(shù)返回時(shí),保存了當(dāng)前的執(zhí)行環(huán)境,也就是會(huì)在 show 函數(shù)中使用的外部變量 n 。
因?yàn)?n 是一個(gè) callFunc 函數(shù)中的局部變量,正常情況下 callFunc 函數(shù)執(zhí)行結(jié)束后,n 就會(huì)被釋放。
但是現(xiàn)在因?yàn)?callFunc 函數(shù)中返回了 show 函數(shù),show 函數(shù)在外部還會(huì)再執(zhí)行,所以程序會(huì)將 show 函數(shù)所需的執(zhí)行環(huán)境保存下來。
這種形式就是閉包。
? 利用閉包完成棋子的移動(dòng)
'''閉包實(shí)現(xiàn)棋子移動(dòng)'''
# 定義一個(gè)外部函數(shù)
def outer():
? ? # 在外部函數(shù)中定義一個(gè)保存坐標(biāo)的列表
? ? position = [0,0]
? ? # 定義一個(gè)內(nèi)部函數(shù),參數(shù)為移動(dòng)方式和步長(zhǎng)
? ? # 移動(dòng)方式為列表 [x,y] x,y分別只能取 -1,0,1三個(gè)值,表示反向,不動(dòng),正向
? ? def inner(direction,step):
? ? ? ? # 計(jì)算坐標(biāo)值
? ? ? ? position[0] = position[0] + direction[0] * step
? ? ? ? position[1] = position[1] + direction[1] * step
? ? ? ? # 返回移動(dòng)后的坐標(biāo)
? ? ? ? return position
? ? # 返回內(nèi)部函數(shù)
? ? return inner
# 獲取內(nèi)部函數(shù)
move = outer()
# 移動(dòng)
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ù)的變量時(shí),不需要任何操作,直接使用就可以了。
但是如果要修改外部變量的值,需要將變量聲明為 nonlocal
def callFunc():
? ? m = 1
? ? n = 2
? ? def show():
? ? ? ? print('show - m: ', m)
? ? ? ? nonlocal n #如果不加會(huì)報(bào)錯(cuò)。
? ? ? ? n *= 10
? ? ? ? print('show - n: ', n)
? ? return show
s = callFunc()
s()
nonlocal 聲明變量為非本地變量,如果確定在程序要修改外部變量,那么建議將 nonlocal 寫在內(nèi)部函數(shù)的第一行。
小結(jié): 閉包就是在一個(gè)外部函數(shù)中定義了一個(gè)內(nèi)部函數(shù),并且在內(nèi)部函數(shù)中使用了外部函數(shù)的變量,并返回了內(nèi)部函數(shù)。
1.8.5 裝飾器
? 裝飾器的定義:
? ? ? 不改變?cè)泻瘮?shù)功能的基礎(chǔ)上,對(duì)函數(shù)進(jìn)行擴(kuò)展的形式,稱為裝飾器。
? 裝飾器的本質(zhì):
? ? ? 實(shí)際上就是一個(gè)以閉包的形式定義的函數(shù) 。
? 裝飾器的作用:
? ? ? 為現(xiàn)有存在的函數(shù),在不改變函數(shù)的基礎(chǔ)上去增加一些功能進(jìn)行裝飾。
? 裝飾器函數(shù)的使用:
? ? ? 在被裝飾的函數(shù)的前一行,使用 @xxx (@裝飾器(閉包)函數(shù)名) 形式來裝飾
? 裝飾器的好處:
? ? ? 定義好了裝飾器(閉包)函數(shù)后,只需要通過 @xxx (@裝飾器(閉包)函數(shù)名)形式的裝飾器語(yǔ)法,將 @xxx (@裝飾器(閉包)函數(shù)名) 加到要裝飾的函數(shù)前即可。
? 裝飾器的原理:
? ? ? 在執(zhí)行 @xxx 時(shí) ,實(shí)際就是將 原函數(shù)傳遞到閉包函數(shù)中,然后原函數(shù)的引用指向閉包返回的裝飾過的內(nèi)部函數(shù)的引用。
? ? ? @count_time? ? # 這實(shí)際就相當(dāng)于解決方法3中的 my_count = count_tiem(my_count)(把被裝飾的函數(shù)引用當(dāng)作參數(shù)傳入到裝飾器函數(shù),被裝飾的函數(shù)引用指向裝飾器函數(shù)返回的引用)
實(shí)例應(yīng)用
現(xiàn)在一個(gè)項(xiàng)目中,有很多函數(shù) ,由于項(xiàng)目越來越大,功能越來越多,導(dǎo)致程序越來越慢。
其中一個(gè)功能函數(shù)功能,實(shí)現(xiàn)一百萬(wàn)次的累加。
def my_count():
? ? s = 0
? ? for i in range(1000001):
? ? ? ? s += i
? ? print('sum : ', s)
現(xiàn)在想計(jì)算一下函數(shù)的運(yùn)行時(shí)間,如何解決?如何能應(yīng)用到所有函數(shù)上?
解決辦法 1
start = time.time()
my_count()
end = time.time()
print('共計(jì)執(zhí)行:%s 秒'%(end - start)) # 使用%d顯示,取整后是0秒,因?yàn)椴坏揭幻?/p>
這種辦法是最簡(jiǎn)單的實(shí)現(xiàn)方式,但是一個(gè)函數(shù)沒問題,但是要有1000個(gè)函數(shù),那么每個(gè)函數(shù)都要寫一遍,非常麻煩并且代碼量憑空多了三千行。
這明顯是不符合開發(fā)的原則的,代碼太冗余
解決辦法 2
def count_time(func):
? ? start = time.time()
? ? func()
? ? end = time.time()
? ? print('共計(jì)執(zhí)行:%s 秒'%(end - start)) # 使用%d顯示,取整后是0秒,因?yàn)椴坏揭幻?/p>
count_time(my_count)
經(jīng)過修改后,定了一個(gè)函數(shù)來實(shí)現(xiàn)計(jì)算時(shí)間的功能,通過傳參,將需要計(jì)算的函數(shù)傳遞進(jìn)去,進(jìn)行計(jì)算。
修改后的代碼,比之前好很多。
但是在使用時(shí),還是需要將函數(shù)傳入到時(shí)間計(jì)算函數(shù)中。
能不能實(shí)現(xiàn)在使用時(shí),不影響函數(shù)原來的使用方式,而又能實(shí)現(xiàn)計(jì)算功能呢?
解決辦法 3
def count_time(func):
? ? def wrapper():? ? ? #wrapper 裝飾
? ? ? ? start = time.time()
? ? ? ? func()
? ? ? ? end = time.time()
? ? ? ? print('共計(jì)執(zhí)行:%s 秒'%(end - start)) # 使用%d顯示,取整后是0秒,因?yàn)椴坏揭幻?/p>
? ? return wrapper
my_count = count_time(my_count)
my_count()
此次在解釋辦法2的基礎(chǔ)上,又將功能外添加了一層函數(shù)定義,實(shí)現(xiàn)了以閉包的形式來進(jìn)行定義
在使用時(shí),讓 my_count 函數(shù)重新指向了 count_time 函數(shù)返回后的函數(shù)引用。這樣在使用 my_count 函數(shù)時(shí),就和原來使用方式一樣了。
這種形式實(shí)際上就是塌裝飾器的實(shí)現(xiàn)原理。
之前我們用過裝飾器,如:@property 等
那么是否可以像系統(tǒng)裝飾器一樣改進(jìn)呢?
解決辦法 4
import time
def count_time(func):
? ? def wrapper():? ? ? #wrapper 裝飾
? ? ? ? start = time.time()
? ? ? ? func()
? ? ? ? end = time.time()
? ? ? ? print('共計(jì)執(zhí)行:%s 秒'%(end - start)) # 使用%d顯示,取整后是0秒,因?yàn)椴坏揭幻?/p>
? ? return wrapper
@count_time? ? # 這實(shí)際就相當(dāng)于解決方法3中的 my_count = count_tiem(my_count)
def my_count():
? ? s = 0
? ? for i in range(10000001):
? ? ? ? s += i
? ? print('sum : ', s)
my_count()
這樣實(shí)現(xiàn)的好處是,定義好了閉包函數(shù)后。只需要通過 @xxx 形式的裝飾器語(yǔ)法,將 @xxx 加到要裝飾的函數(shù)前即可。
使用者在使用時(shí),根本不需要知道被裝飾了。只需要知道原來的函數(shù)功能是什么即可。
這種不改變?cè)泻瘮?shù)功能基礎(chǔ)上,對(duì)函數(shù)進(jìn)行擴(kuò)展的形式,稱為裝飾器。
在執(zhí)行 @xxx 時(shí) ,實(shí)際就是將 原函數(shù)傳遞到閉包中,然后原函數(shù)的引用指向閉包返回的裝飾過的內(nèi)部函數(shù)的引用。
1.8.6 裝飾器的幾種形式
根據(jù)被裝飾函數(shù)定義的參數(shù)和返回值定義形式不同,裝飾器也對(duì)應(yīng)幾種變形。
? 無參無返回值
? ? 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 萬(wàn)能裝飾器
萬(wàn)能裝飾器的定義:
通過可變參數(shù)和關(guān)鍵字參數(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 類實(shí)現(xiàn)裝飾形式
通過類的定義實(shí)現(xiàn)裝飾器形式:
? 在類中通過使用 __init__ 和 __call__方法來實(shí)現(xiàn)
? 通過重寫__init__初始化方法,接收參數(shù),將要被裝飾的函數(shù)傳進(jìn)來并記錄下來(相當(dāng)于外函數(shù)接收參數(shù))
? 通過重寫 __call__ 方法來實(shí)現(xiàn)裝飾內(nèi)容(相當(dāng)于內(nèi)函數(shù)實(shí)現(xiàn)裝飾內(nèi)容)
? @Test? ——》 show = Test(show) show由原來引用函數(shù),裝飾后變成 引用Test裝飾類的對(duì)象
? show() ——》實(shí)際上是仿函數(shù)(是在實(shí)現(xiàn)__call__魔法方法后,將對(duì)象當(dāng)做函數(shù)一樣去使用),即對(duì)象調(diào)用方法實(shí)現(xiàn)了裝飾器
? ? class Test(object):
? ? ? ? # 通過初始化方法,將要被裝飾的函數(shù)傳進(jìn)來并記錄下來
? ? ? ? def __init__(self, func):
? ? ? ? ? ? self.__func = func
? ? ? ? # 重寫 __call__ 方法來實(shí)現(xiàn)裝飾內(nèi)容
? ? ? ? def __call__(self, *args, **kwargs):
? ? ? ? ? ? print('wrapper context')
? ? ? ? ? ? self.__func(*args, **kwargs)
? ? # 實(shí)際通過類的魔法方法call來實(shí)現(xiàn)
? ? @Test? # --> show = Test(show) show由原來引用函數(shù),裝飾后變成引用Test裝飾類的對(duì)象
? ? def show():
? ? ? ? pass
? ? show()? # 對(duì)象調(diào)用方法,實(shí)際上是調(diào)用魔法方法call,實(shí)現(xiàn)了裝飾器
1.8.9 函數(shù)被多個(gè)裝飾器所裝飾(了解)
一個(gè)函數(shù)在使用時(shí),通過一個(gè)裝飾器來擴(kuò)展,可能并不能完成達(dá)到預(yù)期。
Python 中允許一個(gè)裝飾器裝飾多個(gè)函數(shù)和一個(gè)函數(shù)被多個(gè)裝飾器所裝飾。
? 多個(gè)裝飾器的裝飾過程:
? ? ? 從下向上裝飾(即從里往外執(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
#一個(gè)函數(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...
這個(gè)裝飾器的裝飾過程是
從下向上裝飾,即從里往外執(zhí)行,先裝飾函數(shù),然后再一層一層裝飾。
@setFunc2 -> show = setFunc2(show) -> show = setFunc2.wrapper2 @setFunc1 -> show = setFunc1(setFunc2.wrapper2) -> show = setFunc1.wrapper1(setFunc2.wrapper2(show))
1.8.10 裝飾器傳參:
裝飾器在使用過程中,可能需要對(duì)裝飾器進(jìn)行傳參
? 在定義可以傳參的裝飾器閉包時(shí),需要定義三層函數(shù)
? 最外層函數(shù)用來接收裝飾器的參數(shù)
? 中間層用來實(shí)現(xiàn)裝飾器
? 最內(nèi)層用來執(zhí)行具體的裝飾內(nèi)容
? 無論有幾層或者幾個(gè)裝飾器去裝飾已有函數(shù),最終函數(shù)都是引用裝飾器的最內(nèi)層的函數(shù)。
? @xxx(xxx)? 先執(zhí)行傳參 xxx(xxx) ,實(shí)際就是執(zhí)行函數(shù)調(diào)用,得到中間層函數(shù), 與@組合后變成裝飾器形式,再進(jìn)行裝飾
# 定義一個(gè)路由字典
router = {}
# 實(shí)現(xiàn)一個(gè)裝飾器,讓這個(gè)裝飾器來實(shí)現(xiàn)自動(dòng)將 url 和 功能函數(shù)的匹配關(guān)系存到路由字典中
# 接收url參數(shù)
def set_args(args):
? ? # 真正用來去裝飾接收的函數(shù)
? ? def set_func(func):
? ? ? ? # 真正裝飾函數(shù)
? ? ? ? def wrapper(*args, **kwargs):
? ? ? ? ? ? func()
? ? ? ? # 因?yàn)?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('雙十一')
# 模擬的運(yùn)行函數(shù)
def run(url):
? ? # 通過傳入?yún)?shù),也就是訪問地址,來到路由字典中去找到對(duì)應(yīng)的功能函數(shù)
? ? func = router[url]
? ? # 執(zhí)行相應(yīng)的功能函數(shù)
? ? func()
# 模擬請(qǐng)求
run('login.html')
run('nba.html')
run('news.html')
run('11.html')
print(router)
1.8.11 總結(jié):
? 1. 函數(shù)可以像普通變量一樣,做為函數(shù)的參數(shù)或返回值進(jìn)行傳遞
? 2. 函數(shù)內(nèi)部可以定義另外一個(gè)函數(shù),這樣做的目的可以隱藏函數(shù)功能的實(shí)現(xiàn)
? 3. 閉包實(shí)際也是一種函數(shù)定義形式。
? 4. 閉包定義規(guī)則是在外部函數(shù)中定義一個(gè)內(nèi)部函數(shù),內(nèi)部函數(shù)使用外部函數(shù)的變量,并返回內(nèi)部函數(shù)的引用
? 5. Python 中裝飾器就是由閉包來實(shí)現(xiàn)的
? 6. 裝飾器的作用是在不改變現(xiàn)有函數(shù)基礎(chǔ)上,為函數(shù)增加功能。
? 7. 通過在已有函數(shù)前,通過 @閉包函數(shù)名 的形式來給已有函數(shù)添加裝飾器
? 8. 裝飾器函數(shù)根據(jù)參數(shù)和返回值的不同,可細(xì)分為四種定義形式
? 9. 可以通過可變參數(shù)和關(guān)鍵字參數(shù)來實(shí)現(xiàn)能用裝飾器定義形式
? 10. 一個(gè)裝飾器可以為多個(gè)函數(shù)提供裝飾功能,只需要在被裝飾的函數(shù)前加 @xxx 即可
? 11. 通過類也可以實(shí)現(xiàn)裝飾器效果,需要重寫 __init__ 和 __call__ 函數(shù)
? 12. 類實(shí)現(xiàn)的裝飾器在裝飾函數(shù)后,原來的函數(shù)引用不在是函數(shù),而是裝飾類的對(duì)象
? 13. 一個(gè)函數(shù)也可以被多個(gè)裝飾器所裝飾,但是實(shí)際在使用時(shí),并不多見,了解形式即可