詳解python裝飾器(二)
上一篇文章介紹了裝飾器的基本概念和語法,並且實現了一個簡單的裝飾器。但這些僅僅是學習裝飾器的第一步,本文介紹如何實現一個更好的裝飾器。
保留函式屬性
上一篇文章已經提到在python中函式也是一個物件,並且使用了它的__name__
屬性。事實上,python中的函式作為一個function
類的一個例項,有很多的屬性,其中比較重要的屬性石是__name__
和__doc__
, 前者已經介紹過了,後者是一個函式的說明文件。
>>> def add(a, b): # 1
... '''
... the sum of a and b
... '''
... return a + b
...
>>> print(add.__name__) # 2
add
>>> print(add.__doc__) # 3
the sum of a and b
- 定義一個函式,併為其寫一個說明文件
- 函式的
__name__
屬性,一般是定義的時候的變數名 - python會自動將函式定義語句後的字串賦值給函式的
__doc__
屬性(定義類的時候也是)。可以通過print(add.__doc__)
或者help(add)
來檢視這個說明文件,建議第二種方式。
定義一個裝飾器, 並應用於add
>>> def decorator (func): # 1
... def wrap(*args, **kwargs):
... # 2
... '''
... inner wrapper
... '''
... # do something
... return func(*args, **kwargs)
... return wrap
...
>>> add = decorator(add) # 3
>>>
>>> add.__name__ # 4
'wrap'
>>>
>>> print(add.__doc__) $# 5
inner wrapper
- 定義一個裝飾器
- 為內部函式寫一個說明文件
- 將裝飾器應用於
add
(作用和使用@
是一樣的) add
的__name__
屬性變了__doc__
屬性同樣變了
將裝飾器應用於add
之後,add
所指向的物件實際上是decorator
內部定義的函式wrap
,只是二者的功能是一樣的,但是它的一些屬性變得和原來不在一樣以後,這顯然不是我們想要的。所以在定義裝飾器的時候需要一點修改。
>>> def decorator(func):
... def wrap(*args, **kwargs):
... '''
... inner wrapper
... '''
... # do something
... return func(*args, **kwargs)
... wrap.__name__ = func.__name__ # 顯示的修改wrap的屬性
... wrap.__doc__ = func.__doc__
... return wrap
python為我們提供了更簡單的方式。
>>> import functools
>>> def decorator(func):
... @functool.wraps(func) # python內建的裝飾器
... def wrap(*args, **kwargs):
... '''
... inner wrapper
... '''
... # do something
... return func(*args, **kwargs)
... return wrap
可以看到,我們使用了一個新的裝飾器,這個裝飾器和我們之前所使用的裝飾器都不太一樣,因為它接收了一個引數func
。接下來介紹,帶引數的裝飾器。
引數化的裝飾器
函式可以根據引數返回對應的結果,在python中這個返回值可以是一個函式。既然裝飾器是一個可以返回函式的函式,那自然也可以定義一個工廠函式,讓它根據不同的引數返回訂製的裝飾器。
>>> import time
>>> def timeit(func):
... def wrapper(*args, **kwargs):
... start_time = time.time()
... result = func(*args, **kwargs)
... print('<run %s, cost time:%.4fs>' %(func.__name__, time.time() - start_time))
... return result
... return wrapper
這是上一篇文章中的例子,功能是在函式執行完成後列印執行時長。假如現在有一個需求,可以訂製精確到小數點後幾位。我們不用針對每一個精度都寫一個對應的裝飾器,只需要實現一個裝飾器工廠函式,根據不同的精度,返回不同的裝飾器。
>>> def timeit_factory(i): # 1
... fmt = 'cost time:{:.%df}s' % i # 2
... def timeit(func): # 3
... @functools.wraps(func)
... def wrapper(*args, **kwargs):
... start_time = time.time()
... result = func(*args, **kwargs)
... cost = time.time() - start_time
... print(fmt.format(cost))
... return result
... return wrapper
... return timeit
- 定義裝飾器工廠函式
- 這個稍後再講
- 定義裝飾器
上述程式碼實現了一個裝飾器工廠函式,根據不同的引數生成不同的fmt
,繼而控制精確到小數點後幾位。
>>> @timeit_factory(2)
... def f2(a, b):return a + b
...
>>> @timeit_factory(4)
... def f4(a, b):return a + b
...
>>> f2(1, 2)
cost time:0.00s
3
>>> f4(1, 2)
cost time:0.0000s
3
閉包
上一段程式碼中,在timeit
之前,也就是真的裝飾器之前,定義了一個fmt
,然後在裝飾器內部使用這個變數,讓我們分析一下這個裝飾器載入過程。
- 呼叫
timeit_factory
,並定引數 - 定義
fmt
- 定義裝飾器
- 在裝飾器內部使用了
fmt
,但是並沒有儲存 - 返回裝飾器,
timeit_factory
函式結束,同時區域性名稱空間也被銷燬
那麼問題來了,再後來的函式呼叫中,是肯定使用到了fmt
,但是這個fmt
儲存到哪裡了呢。並且又上面的例子可以看出,針對f2
和f4
,應該是有兩個取值不同的fmt
。
如果這個例子不夠清晰,再舉一個例子。
現在需要一個函式,功能是計算不斷增加的一系列值的平均值。需要實現的功能如下
>>> avg(4)
4.0
>>> avg(5)
4.5
>>> avg(9)
6.0
要實現這個功能肯定是需要儲存每次呼叫時輸入進去的值,問題在於儲存在哪裡呢。 可以用一個類來實現這個功能。
>>> class Average():
... def __init__(self):
... self.sum = 0
... self.size = 0
... def __call__(self, val):
... self.sum += val
... self.size += 1
... return self.sum / self.size
定義了__call__
屬性後的類,其例項可以像一個函式一樣被呼叫,具體不在此說明。
在python中,還有另一種實現方法,不需要類。
>>> def Average():
... total = 0
... size = 0
... def avg(val):
... nonlocal total, size # 標明引用外部變數
... total += val
... size += 1
... return total / size
... return avg
>>> avg(4)
4.0
>>> avg(5)
4.5
>>> avg2 = Average()
>>> avg2(3)
3.0
>>> avg2(5)
4.0
看出看出,這個函式同樣滿足需求,並且返回的兩個函式avg
和avg2
並不相互影響。
在函式avg內沒有定義total
和sum
,那麼後來使用的這兩個變數存在哪裡了?
答案是閉包
維基百科給閉包的定義:引用了自由變數的函式。這個被引用的自由變數將和這個函式一同存在,即使已經離開了創造它的環境也不例外。
也就是說,avg
和avg2
引用了自由變數,也就是外部定義的變數,那麼這個變數將和這兩個函式一起存在,即使定義這些自由變數的環境後也不例外。
>>> avg.__code__.co_freevars # 1
('size', 'total')
>>> avg.__closure__[0].cell_contents # 2
3
>>> avg.__closure__[1].cell_contents # 3
18
avg
使用的自由變數- 在
avg
的閉包中儲存著size
和total
總結
本文介紹瞭如何實現一個更好的裝飾器(保留函式屬性以及使用裝飾器工廠),並且解釋了自由變數和函式閉包。