python 高階 13閉包 裝飾器
閉包和裝飾器
1.8 閉包和裝飾器
學習目標
1. 能夠說出閉包的定義形式
2. 能夠說出裝飾器的實現形式
3. 能夠說出裝飾器的作用
4. 能夠說出裝飾器的不同形式
5. 能夠說出萬能裝飾器的實現形式
6. 能夠說出裝飾器的執行過程
--------------------------------------------------------------------------------
1.8.1 閉包和裝飾器概述
什麼是閉包:
閉包是指在一個函式中定義了一個另外一個函式,內函式裡運用了外函式的臨時變數(實際引數也是臨時變數),並且外函式的返回值是內函式的引用(一切皆引用,所有的函式名字都只是函式體在記憶體空間的一個引用。)
閉包的作用:
可以隱藏內部函式的工作細節,只給外部使用者提供一個可以執行的內部函式的引用。
避免了使用全域性變數,保證了程式的封裝性
保證了內函式的安全性,其他函式不能訪問
什麼是裝飾器:
裝飾器就是用於拓展已有函式功能的一種函式,這個函式的特殊之處在於它的返回值也是一個函式,實際上就是利用閉包語法實現的。
裝飾器的作用
在不用更改原函式的程式碼前提下給函式增加新的功能。
1.8.2 思考
有一種叫做五步棋的遊戲,在一個 五行五列的網格上雙方各持一色棋子,在棋子移動時,只能以橫向或縱向的形式移動。
如果實現遊戲,如何能記錄當前棋子移動的位置(也就是座標)呢?
1.8.3 技術點回顧
在使用函式時,函式可以傳遞引數,函式也可以返回資料。
在傳遞和返回資料時,一般是傳遞返回的固定資料和程式碼執行的結果。
在Pyhton中,函式也是一個物件,在函式操作中,函式物件也可以當成一個引數或一個返回值進行返回。
當程式內或程式外拿到引數的引用後就可以直接使用這個函式,(原理回想下深淺拷貝中的賦值)
def show():
print('show run')
show()
func = show
func()
程式執行結果:
show run
show run
1.8.4 閉包
閉包就是在一個外部函式中定義了一個內部函式,並且在內部函式中使用了外部函式的變數,並返回了內部函式的引用。
nonlocal 的使用
nonlocal 變數名 ——》宣告變數為非本地變數
如果在閉包的內部函式中直接使用外部函式的變數時,不需要任何操作,直接使用就可以了。
但是如果要修改外部變數的值,需要將變數宣告為 nonlocal,那麼建議將 nonlocal 寫在內部函式的第一行。
利用函式可以被傳遞和返回的特性,在開發過程中,可以隱藏更多的實現細節。
n = 1 # 全域性變數
def show(): # 公有函式
print('show: ',n)
def callFunc(func): #公有函式
return func
s = callFunc(show) # 函式執行
s()
show()
在這段程式碼中,在實際開發中並沒有實際意義,只是簡單示意了函式可以被當做引數和返回值使用。
但是這段程式碼並不完美
第一,儘量不要使用全域性變數,因為全域性變數會破壞程式的封裝性。
第二,如果 show 函式不想被 callFunc 以外的函式進行訪問時,是無法控制的。
所以可以改進如下:
def callFunc():
n = 1
def show():
print('show: ', n)
return show
s = callFunc()
s()
# show() 因為 show 函式定義在 callFunc 內部,所以外部不可見,不能使用
程式碼改進後,去掉了全域性變數的使用。而且將 show 函式封裝在了 callFunc 函式內部,使外部不可見,不能使用 show 函式,隱藏了實現細節
程式在執行時,callFunc 函式返回了內部定義的 show 函式,並且 在 show 函式內部使用了外部函式的變數。
在 show 函式返回時,儲存了當前的執行環境,也就是會在 show 函式中使用的外部變數 n 。
因為 n 是一個 callFunc 函式中的區域性變數,正常情況下 callFunc 函式執行結束後,n 就會被釋放。
但是現在因為 callFunc 函式中返回了 show 函式,show 函式在外部還會再執行,所以程式會將 show 函式所需的執行環境儲存下來。
這種形式就是閉包。
利用閉包完成棋子的移動
'''閉包實現棋子移動'''
# 定義一個外部函式
def outer():
# 在外部函式中定義一個儲存座標的列表
position = [0,0]
# 定義一個內部函式,引數為移動方式和步長
# 移動方式為列表 [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
# 返回內部函式
return inner
# 獲取內部函式
move = outer()
# 移動
print(move([1, 0], 10))
print(move([0, 1], 10))
print(move([-1, 0], 10))
程式執行結果:
[10, 0]
[10, 10]
[0, 10]
nonlocal 的使用 如果在閉包的內部函式中直接使用外部函式的變數時,不需要任何操作,直接使用就可以了。
但是如果要修改外部變數的值,需要將變數宣告為 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 寫在內部函式的第一行。
小結: 閉包就是在一個外部函式中定義了一個內部函式,並且在內部函式中使用了外部函式的變數,並返回了內部函式。
1.8.5 裝飾器
裝飾器的定義:
不改變原有函式功能的基礎上,對函式進行擴充套件的形式,稱為裝飾器。
裝飾器的本質:
實際上就是一個以閉包的形式定義的函式 。
裝飾器的作用:
為現有存在的函式,在不改變函式的基礎上去增加一些功能進行裝飾。
裝飾器函式的使用:
在被裝飾的函式的前一行,使用 @xxx (@裝飾器(閉包)函式名) 形式來裝飾
裝飾器的好處:
定義好了裝飾器(閉包)函式後,只需要通過 @xxx (@裝飾器(閉包)函式名)形式的裝飾器語法,將 @xxx (@裝飾器(閉包)函式名) 加到要裝飾的函式前即可。
裝飾器的原理:
在執行 @xxx 時 ,實際就是將 原函式傳遞到閉包函式中,然後原函式的引用指向閉包返回的裝飾過的內部函式的引用。
@count_time # 這實際就相當於解決方法3中的 my_count = count_tiem(my_count)(把被裝飾的函式引用當作引數傳入到裝飾器函式,被裝飾的函式引用指向裝飾器函式返回的引用)
例項應用
現在一個專案中,有很多函式 ,由於專案越來越大,功能越來越多,導致程式越來越慢。
其中一個功能函式功能,實現一百萬次的累加。
def my_count():
s = 0
for i in range(1000001):
s += i
print('sum : ', s)
現在想計算一下函式的執行時間,如何解決?如何能應用到所有函式上?
解決辦法 1
start = time.time()
my_count()
end = time.time()
print('共計執行:%s 秒'%(end - start)) # 使用%d顯示,取整後是0秒,因為不到一秒
這種辦法是最簡單的實現方式,但是一個函式沒問題,但是要有1000個函式,那麼每個函式都要寫一遍,非常麻煩並且程式碼量憑空多了三千行。
這明顯是不符合開發的原則的,程式碼太冗餘
解決辦法 2
def count_time(func):
start = time.time()
func()
end = time.time()
print('共計執行:%s 秒'%(end - start)) # 使用%d顯示,取整後是0秒,因為不到一秒
count_time(my_count)
經過修改後,定了一個函式來實現計算時間的功能,通過傳參,將需要計算的函式傳遞進去,進行計算。
修改後的程式碼,比之前好很多。
但是在使用時,還是需要將函式傳入到時間計算函式中。
能不能實現在使用時,不影響函式原來的使用方式,而又能實現計算功能呢?
解決辦法 3
def count_time(func):
def wrapper(): #wrapper 裝飾
start = time.time()
func()
end = time.time()
print('共計執行:%s 秒'%(end - start)) # 使用%d顯示,取整後是0秒,因為不到一秒
return wrapper
my_count = count_time(my_count)
my_count()
此次在解釋辦法2的基礎上,又將功能外添加了一層函式定義,實現了以閉包的形式來進行定義
在使用時,讓 my_count 函式重新指向了 count_time 函式返回後的函式引用。這樣在使用 my_count 函式時,就和原來使用方式一樣了。
這種形式實際上就是塌裝飾器的實現原理。
之前我們用過裝飾器,如:@property 等
那麼是否可以像系統裝飾器一樣改進呢?
解決辦法 4
import time
def count_time(func):
def wrapper(): #wrapper 裝飾
start = time.time()
func()
end = time.time()
print('共計執行:%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()
這樣實現的好處是,定義好了閉包函式後。只需要通過 @xxx 形式的裝飾器語法,將 @xxx 加到要裝飾的函式前即可。
使用者在使用時,根本不需要知道被裝飾了。只需要知道原來的函式功能是什麼即可。
這種不改變原有函式功能基礎上,對函式進行擴充套件的形式,稱為裝飾器。
在執行 @xxx 時 ,實際就是將 原函式傳遞到閉包中,然後原函式的引用指向閉包返回的裝飾過的內部函式的引用。
1.8.6 裝飾器的幾種形式
根據被裝飾函式定義的引數和返回值定義形式不同,裝飾器也對應幾種變形。
無參無返回值
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 萬能裝飾器
萬能裝飾器的定義:
通過可變引數和關鍵字引數來接收不同的引數型別定義出來的裝飾器函式適用於任何形式的函式。
def setFunc(func):
def wrapper(*args, **kwargs): # 接收不同的引數
print('wrapper context')
return func(*args, *kwargs) # 再原樣傳回給被裝飾的函式
return wrapper
@setFunc
def show(name, age):
print(name,age)
show('tom',12)
1.8.8 類實現裝飾形式
通過類的定義實現裝飾器形式:
在類中通過使用 __init__ 和 __call__方法來實現
通過重寫__init__初始化方法,接收引數,將要被裝飾的函式傳進來並記錄下來(相當於外函式接收引數)
通過重寫 __call__ 方法來實現裝飾內容(相當於內函式實現裝飾內容)
@Test ——》 show = Test(show) show由原來引用函式,裝飾後變成 引用Test裝飾類的物件
show() ——》實際上是仿函式(是在實現__call__魔法方法後,將物件當做函式一樣去使用),即物件呼叫方法實現了裝飾器
class Test(object):
# 通過初始化方法,將要被裝飾的函式傳進來並記錄下來
def __init__(self, func):
self.__func = func
# 重寫 __call__ 方法來實現裝飾內容
def __call__(self, *args, **kwargs):
print('wrapper context')
self.__func(*args, **kwargs)
# 實際通過類的魔法方法call來實現
@Test # --> show = Test(show) show由原來引用函式,裝飾後變成引用Test裝飾類的物件
def show():
pass
show() # 物件呼叫方法,實際上是呼叫魔法方法call,實現了裝飾器
1.8.9 函式被多個裝飾器所裝飾(瞭解)
一個函式在使用時,通過一個裝飾器來擴充套件,可能並不能完成達到預期。
Python 中允許一個裝飾器裝飾多個函式和一個函式被多個裝飾器所裝飾。
多個裝飾器的裝飾過程:
從下向上裝飾(即從裡往外執行),先裝飾函式,然後再向外(向上)一層一層裝飾。
# 裝飾器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
#一個函式被裝飾了兩次
@setFunc1
@setFunc2
def show(*args, **kwargs):
print('Show Run ...')
show()
程式執行結果 :
Wrapper Context 1 Start...
Wrapper Context 2 Start...
Show Run ...
Wrapper Context 2 End...
Wrapper Context 1 End...
這個裝飾器的裝飾過程是
從下向上裝飾,即從裡往外執行,先裝飾函式,然後再一層一層裝飾。
@setFunc2 -> show = setFunc2(show) -> show = setFunc2.wrapper2 @setFunc1 -> show = setFunc1(setFunc2.wrapper2) -> show = setFunc1.wrapper1(setFunc2.wrapper2(show))
1.8.10 裝飾器傳參:
裝飾器在使用過程中,可能需要對裝飾器進行傳參
在定義可以傳參的裝飾器閉包時,需要定義三層函式
最外層函式用來接收裝飾器的引數
中間層用來實現裝飾器
最內層用來執行具體的裝飾內容
無論有幾層或者幾個裝飾器去裝飾已有函式,最終函式都是引用裝飾器的最內層的函式。
@xxx(xxx) 先執行傳參 xxx(xxx) ,實際就是執行函式呼叫,得到中間層函式, 與@組合後變成裝飾器形式,再進行裝飾
# 定義一個路由字典
router = {}
# 實現一個裝飾器,讓這個裝飾器來實現自動將 url 和 功能函式的匹配關係存到路由字典中
# 接收url引數
def set_args(args):
# 真正用來去裝飾接收的函式
def set_func(func):
# 真正裝飾函式
def wrapper(*args, **kwargs):
func()
# 因為 wrapper 就是指向被裝飾的函式
router[args] = wrapper
return wrapper
return set_func
# 功能函式
# @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('雙十一')
# 模擬的執行函式
def run(url):
# 通過傳入引數,也就是訪問地址,來到路由字典中去找到對應的功能函式
func = router[url]
# 執行相應的功能函式
func()
# 模擬請求
run('login.html')
run('nba.html')
run('news.html')
run('11.html')
print(router)
1.8.11 總結:
1. 函式可以像普通變數一樣,做為函式的引數或返回值進行傳遞
2. 函式內部可以定義另外一個函式,這樣做的目的可以隱藏函式功能的實現
3. 閉包實際也是一種函式定義形式。
4. 閉包定義規則是在外部函式中定義一個內部函式,內部函式使用外部函式的變數,並返回內部函式的引用
5. Python 中裝飾器就是由閉包來實現的
6. 裝飾器的作用是在不改變現有函式基礎上,為函式增加功能。
7. 通過在已有函式前,通過 @閉包函式名 的形式來給已有函式新增裝飾器
8. 裝飾器函式根據引數和返回值的不同,可細分為四種定義形式
9. 可以通過可變引數和關鍵字引數來實現能用裝飾器定義形式
10. 一個裝飾器可以為多個函式提供裝飾功能,只需要在被裝飾的函式前加 @xxx 即可
11. 通過類也可以實現裝飾器效果,需要重寫 __init__ 和 __call__ 函式
12. 類實現的裝飾器在裝飾函式後,原來的函式引用不在是函式,而是裝飾類的物件
13. 一個函式也可以被多個裝飾器所裝飾,但是實際在使用時,並不多見,瞭解形式即可