Python 簡明教程 — 22,Python 閉包與裝飾器
- 2020 年 7 月 5 日
- 筆記
- Python 簡明教程
微信公眾號:碼農充電站pro
個人主頁://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 簡明教程 — 20,Python 類中的屬性與方法
歡迎關注作者公眾號,獲取更多技術乾貨。