1. 程式人生 > >詳解python裝飾器(二)

詳解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
  1. 定義一個函式,併為其寫一個說明文件
  2. 函式的__name__屬性,一般是定義的時候的變數名
  3. 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
  1. 定義一個裝飾器
  2. 為內部函式寫一個說明文件
  3. 將裝飾器應用於add(作用和使用@是一樣的)
  4. add__name__屬性變了
  5. __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
  1. 定義裝飾器工廠函式
  2. 這個稍後再講
  3. 定義裝飾器

上述程式碼實現了一個裝飾器工廠函式,根據不同的引數生成不同的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,然後在裝飾器內部使用這個變數,讓我們分析一下這個裝飾器載入過程。

  1. 呼叫timeit_factory,並定引數
  2. 定義fmt
  3. 定義裝飾器
  4. 在裝飾器內部使用了fmt,但是並沒有儲存
  5. 返回裝飾器,timeit_factory函式結束,同時區域性名稱空間也被銷燬

那麼問題來了,再後來的函式呼叫中,是肯定使用到了fmt,但是這個fmt儲存到哪裡了呢。並且又上面的例子可以看出,針對f2f4,應該是有兩個取值不同的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

看出看出,這個函式同樣滿足需求,並且返回的兩個函式avgavg2並不相互影響。

在函式avg內沒有定義totalsum,那麼後來使用的這兩個變數存在哪裡了?

答案是閉包

維基百科給閉包的定義:引用了自由變數的函式。這個被引用的自由變數將和這個函式一同存在,即使已經離開了創造它的環境也不例外。

也就是說,avgavg2引用了自由變數,也就是外部定義的變數,那麼這個變數將和這兩個函式一起存在,即使定義這些自由變數的環境後也不例外。

>>> avg.__code__.co_freevars # 1
('size', 'total')

>>> avg.__closure__[0].cell_contents # 2
3
>>> avg.__closure__[1].cell_contents  # 3
18
  1. avg使用的自由變數
  2. avg的閉包中儲存著sizetotal

總結

本文介紹瞭如何實現一個更好的裝飾器(保留函式屬性以及使用裝飾器工廠),並且解釋了自由變數和函式閉包。