微信公眾號:碼農充電站pro
個人主頁:https://codeshellme.github.io
當你選擇了一種語言,意味著你還選擇了一組技術、一個社群。
目錄
本節我們來介紹閉包
與裝飾器
。
閉包與裝飾器是函式的高階用法,其實在介紹完Python 函式我們就可以介紹本節的內容,但由於Python中的類
也可以用來實現裝飾器,所以我們等到介紹完了Python 類再來統一介紹閉包與裝飾器。
裝飾器使用的是閉包的特性,我們先來介紹閉包,再來介紹裝飾器。
1,什麼是閉包
Python 的函式內部還允許巢狀函式
,也就是一個函式中還定義了另一個函式。如下:
def fun_1():
def fun_2():
return 'hello'
s = fun_2()
return s
s = fun_1()
print(s) # 'hello'
在上面的程式碼中,我們在函式fun_1
的內部又定義了一個函式fun_2
,這就是函式巢狀
。
我們在學習函式的時候,還知道,Python 函式可以作為函式引數
和函式返回值
。
因此,我們可以將上面程式碼中的函式fun_2
作為函式fun_1
的返回值,如下:
def fun_1():
def fun_2():
return 'hello'
return fun_2
此時,函式fun_1
返回了一個函式,我們這樣使用fun_1
:
fun = fun_1() # fun 是一個函式
s = fun() # 呼叫函式 fun
print(s) # s 就是 'hello'
我們再來改進函式fun_1
,如下:
def fun_1(s):
s1 = 'hello ' + s
def fun_2():
return s1
return fun_2
上面的程式碼中,內部函式fun_2
返回了變數s1
,而s1
是函式fun_2
的外部變數
,這種內部函式
能夠使用外部變數
,並且內部函式
作為外部函式
的返回值
,就是閉包
。
編寫閉包時都有一定的套路,也就是,閉包需要有一個外部函式包含一個內部函式,並且外部函式的返回值是內部函式。
2,用閉包實現一個計數器
我們來實現一個計數器的功能,先寫一個框架,如下:
def counter():
# 定義內部函式
def add_one():
pass
# 返回內部函式
return add_one
再來實現計數的功能,如下:
def counter():
# 用於計數
l = [0]
# 定義內部函式
def add_one():
l[0] += 1
return l[0] # 返回數字
# 返回內部函式
return add_one
上面的程式碼中,我們使用了一個列表l[0]
來記錄累加數,在內部函式add_one
中對l[0]
進行累加。
我們這樣使用這個計數器:
c = counter()
print(c()) # 1
print(c()) # 2
print(c()) # 3
我們還可以使這個計數器能夠設定累加的初始值
,就是為counter
函式設定一個引數,如下:
def counter(start):
l = [start]
def add_one():
l[0] += 1
return l[0]
return add_one
這樣我們就可以使用counter
來生成不同的累加器(從不同的初始值開始累加)。我們這樣使用該計數器:
c1 = counter(1) # c1 從 1 開始累加
print(c1()) # 2
print(c1()) # 3
print(c1()) # 4
c5 = counter(5) # c5 從 5 開始累加
print(c5()) # 6
print(c5()) # 7
print(c5()) # 8
c1
從 1
開始累加,c5
從 5
開始累加,兩個互不干擾。
3,什麼是裝飾器
裝飾器
是閉包
的一種進階應用。裝飾器從字面上理解就是用來裝飾
,包裝
的。裝飾器一般用來在不修改函式內部程式碼的情況下,為一個函式新增額外的新功能。
裝飾器雖然功能強大,但也不是萬能的,它也有自己適用場景:
- 快取
- 身份認證
- 記錄函式執行時間
- 輸入的合理性判斷
比如,我們有一個函式,如下:
def hello():
print('hello world.')
如果我們想計算這個函式的執行時間,最直接的想法就是修改該函式,如下:
import time
def hello():
s = time.time()
print('hello world.')
e = time.time()
print('fun:%s time used:%s' % (hello.__name__. e - s))
# 呼叫函式
hello()
其中,time
模組是Python 中的內建模組,用於時間相關計算。
每個函式都有一個__name__
屬性,其值為函式的名字。不管我們是直接檢視一個函式的__name__
屬性,還是將一個函式賦值給一個變數後,再檢視這個變數的__name__
屬性,它們的值都是一樣的(都是原來函式的名字):
print(hello.__name__) # hello
f = hello # 呼叫 f() 和 hello() 的效果是一樣的
print(f.__name__) # hello
但是,如果我們要為很多的函式新增這樣的功能,要是都使用這種辦法,那會相當的麻煩,這時候使用裝飾器就非常的合適。
最簡單的裝飾器
裝飾器應用的就是閉包的特性,所以編寫裝飾器的套路與閉包是一樣的,就是有一個外部函式和一個內部函式,外部函式的返回值是內部函式。
我們先編寫一個框架:
def timer(func):
def wrapper():
pass
return wrapper
再來實現計時功能:
import time
def timer(func):
def wrapper():
s = time.time()
ret = func()
e = time.time()
print('fun:%s time used:%s' % (func.__name__, e - s))
return ret
return wrapper
def hello():
print('hello world.')
該裝飾器的名字是timer
,其接受一個函式型別的引數func
,func
就是要修飾的函式。
func
的函式原型要與內部函式wrapper
的原型一致(這是固定的寫法),即函式引數
相同,函式返回值
也相同。
英文
wrapper
就是裝飾
的意思。
其實timer
就是一個高階函式
,其引數是一個函式型別,返回值也是一個函式。我們可以這樣使用timer
裝飾器:
hello = timer(hello)
hello()
以上程式碼中,hello
函式作為引數傳遞給了timer
裝飾器,返回結果用hello
變數接收,最後呼叫hello()
。這就是裝飾器的原本用法。
只不過,Python 提供了一種語法糖
,使得裝飾器的使用方法更加簡單優雅
。如下:
@timer
def hello():
print('hello world.')
hello()
直接在原函式hello
的上方寫一個語法糖@timer
,其實這個作用就相當於hello = timer(hello)
。
用類實現裝飾器
在上面的程式碼中,是用函式
(也就是timer
函式)來實現的裝飾器,我們也可以用類
來實現裝飾器。
用類
實現裝飾器,主要依賴的是__init__
方法和__call__
方法。
我們知道,實現
__call__
方法的類,其物件可以像函式一樣被呼叫。
用類來實現timer
裝飾器,如下:
import time
class timer:
def __init__(self, func):
self.func = func
def __call__(self):
s = time.time()
ret = self.func()
e = time.time()
print('fun:%s time used:%s' % (self.func.__name__, e - s))
return ret
@timer
def hello():
print('hello world.')
print(hello())
其中,構造方法__init__
接收一個函式型別的引數func
,然後,__call__
方法就相當於wrapper
函式。
用類實現的裝飾器的使用方法,與用函式實現的裝飾器的使用方法是一樣的。
4,被修飾的函式帶有引數
如果hello
函式帶有引數,如下:
def hello(s):
print('hello %s.' % s)
那麼裝飾器應該像下面這樣:
import time
def timer(func):
def wrapper(args):
s = time.time()
ret = func(args)
e = time.time()
print('fun:%s time used:%s' % (func.__name__, e - s))
return ret
return wrapper
@timer
def hello(s):
print('hello %s.' % s)
hello('python')
timer
函式的引數
依然是要被修飾的函式,wrapper
函式的原型與hello
函式保持一致。
用類來實現,如下:
import time
class timer:
def __init__(self, func):
self.func = func
def __call__(self, args):
s = time.time()
ret = self.func(args)
e = time.time()
print('fun:%s time used:%s' % (self.func.__name__, e - s))
return ret
@timer
def hello(s):
print('hello %s.' % s)
print(hello('python'))
不定長引數裝飾器
如果hello
函式的引數是不定長
的,timer
應該是如下這樣:
import time
def timer(func):
def wrapper(*args, **kw):
s = time.time()
ret = func(*args, **kw)
e = time.time()
print('fun:%s time used:%s' % (func.__name__, e - s))
return ret
return wrapper
@timer
def hello(s1, s2): # 帶有兩個引數
print('hello %s %s.' % (s1, s2))
@timer
def hello_java(): # 沒有引數
print('hello java.')
hello('python2', 'python3')
hello_java()
這樣的裝飾器timer
,可以修飾帶有任意引數的函式。
用類來實現,如下:
import time
class timer:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kw):
s = time.time()
ret = self.func(*args, **kw)
e = time.time()
print('fun:%s time used:%s' % (self.func.__name__, e - s))
return ret
@timer
def hello(s1, s2): # 帶有兩個引數
print('hello %s %s.' % (s1, s2))
@timer
def hello_java(): # 沒有引數
print('hello java.')
hello('python2', 'python3')
hello_java()
5,裝飾器帶有引數
如果裝飾器也需要帶有引數,那麼則需要在原來的timer
函式的外層再巢狀一層函式Timer
,Timer
也帶有引數,如下:
import time
def Timer(flag):
def timer(func):
def wrapper(*args, **kw):
s = time.time()
ret = func(*args, **kw)
e = time.time()
print('flag:%s fun:%s time used:%s' % (flag, func.__name__, e - s))
return ret
return wrapper
return timer
@Timer(1)
def hello(s1, s2): # 帶有兩個引數
print('hello %s %s.' % (s1, s2))
@Timer(2)
def hello_java(): # 沒有引數
print('hello java.')
hello('python2', 'python3')
hello_java()
從上面的程式碼中可以看到,timer
的結構沒有改變,只是在wrapper
的內部使用了flag
變數,然後timer
的外層多了一層Timer
,Timer
的返回值是timer
,我們最終使用的裝飾器是Timer
。
我們通過函式.__name__
來檢視函式的__name__
值:
print(hello.__name__) # wrapper
print(hello_java.__name__) # wrapper
可以發現hello
和 hello_java
的__name__
值都是wrapper
(即內部函式wrapper
的名字),而不是hello
和 hello_java
,這並不符合我們的需要,因為我們的初衷只是想增加hello
與hello_java
的功能,但並不想改變它們的函式名字。
6,使用 @functools.wraps
我們可以使用functools
模組的wraps
裝飾器來修飾wrapper
函式,以解決這個問題,如下:
import time
import functools
def Timer(flag):
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kw):
s = time.time()
ret = func(*args, **kw)
e = time.time()
print('flag:%s fun:%s time used:%s' % (flag, func.__name__, e - s))
return ret
return wrapper
return timer
@Timer(1)
def hello(s1, s2): # 帶有兩個引數
print('hello %s %s.' % (s1, s2))
@Timer(2)
def hello_java(): # 沒有引數
print('hello java.')
此時,再檢視hello
與 hello_java
的 __name__
值,分別是hello
和 hello_java
。
7,裝飾器可以疊加使用
裝飾器也可以疊加使用,如下:
@decorator1
@decorator2
@decorator3
def func():
...
上面程式碼的所用相當於:
decorator1(decorator2(decorator3(func)))
8,一個較通用的裝飾器模板
編寫裝飾器有一定的套路,根據上文的介紹,我們可以歸納出一個較通用的裝飾器模板:
def func_name(func_args):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kw):
# 在這裡可以使用func_args,*args,**kw
# 邏輯處理
...
ret = func(*args, **kw)
# 邏輯處理
...
return ret
return wrapper
return decorator
# 使用裝飾器 func_name
@func_name(func_args)
def func_a(*args, **kw):
pass
在上面的模板中:
func_name
是裝飾器的名字,該裝飾器可以接收引數func_args
- 內部函式
decorator
的引數func
,是一個函式型別的引數,就是將來要修飾的函式 func
的引數列表可以是任意的,因為我們使用的是*args, **kw
- 內部函式
wrapper
的原型(即引數與返回值)要與 被修飾的函式func
保持統一 @functools.wraps
的作用是保留被裝飾的原函式的一些元資訊(比如__name__
屬性)
與裝飾器相關的模組有functools
和 wrapt
,可以使用這兩個模組來優化完善你寫的裝飾器,感興趣的小夥伴可以自己擴充學習。
(完。)
推薦閱讀:
Python 簡明教程 --- 17,Python 模組與包
Python 簡明教程 --- 18,Python 物件導向
Python 簡明教程 --- 19,Python 類與物件
Python 簡明教程 --- 20,Python 類中的屬性與方法
Python 簡明教程 --- 21,Python 繼承與多型
歡迎關注作者公眾號,獲取更多技術乾貨。