1. 程式人生 > 實用技巧 >python裝飾器學習詳解-函式部分

python裝飾器學習詳解-函式部分

本文的文字及圖片來源於網路,僅供學習、交流使用,不具有任何商業用途,如有問題請及時聯絡我們以作處理

最近閱讀《流暢的python》看見其用函式寫裝飾器部分寫的很好,想寫一些自己的讀書筆記。眾所周知,裝飾器是python學習過程中的一道門檻,初學者學習時往往是知其然,不知其所以然,這樣的結果是導致一段時間後會遺忘掉該部分內容,只好再次去學習,拉高了學習成本。

想學好python的裝飾器,需要明白一下幾點;

1:閉包

1)函式巢狀

2)內部函式使用外部函式的變數

3)外部函式的返回值為內部函式

​ 接下來看看《流暢的python》中的例子,我稍微修改了一下:

>>> def make_averager(series=[]):
...     def averager(new_value):
...             series.append(new_value)
...             total = sum(series)
...             return total/len(series)
...     return averager
...
>>> avg = make_averager()
>>> avg
<function make_averager.<locals>.averager at 0x10b82cb00>
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0

​ 函式 make_averager 實現了一個 計算當前所有數字的平均值的功能,不斷的新增一個值,然後計算當前的平均值。

​ avg這個物件記憶體地址指向了make_averager這個函式的內部函式中,而且avg通過不斷的新增值進行平均值計算,按理說在這個內部函式沒有儲存new_value的空間,而且在make_averager對avg賦值後,函式返回後series這個變數也應該消失了,但是avg卻依然可以進行計算。

​ 這就是閉包,內部函式averager使用外面的自由變數,也就是屬於make_averager的區域性變數series

>>> avg.__code__.co_varnames
('new_value', 'total')
>>> avg.__code__.co_freevars
('series',)

​ 可以發現avg的自由變數是make_averager的區域性變數,就是說閉包裡的內部函式可以使用外部函式的變數,即我們上面提到的第二點:“內部函式使用外部函式的變數”, 注:自由變數只能read,並不能write,不然會提示本地變數並沒有賦值的錯誤,我們舉的例子沒遇到這個問題,因為我們沒有給 series 賦值,我們只是調 用 series.append,並把它傳給 sum 和 len。也就是說,我們利用了 列表是可變的物件這一事實 。下圖是書中提供的閉包範圍圖:

2:裝飾器的實現

所謂裝飾器,就是在不改變基礎函式的功能上再次給它封裝一層,達到我們想要的目的,接下來我舉個簡單的例子:

​ deco_demo.py

1 def col(func):
  2     def inner(*args, **kwargs):
  3         print(func.__name__)
  4         print(locals())
  5         print(inner.__code__.co_varnames)
  6         print(inner.__code__.co_freevars)
  7         return func(*args, **kwargs)
  8     return inner
  9
 10
 11 @col
 12 def new_add(x):
 13     return x+2
 14
 15
 16 def new_add_1(x):
 17     return x+3
 18
 19
 20 print(new_add(3))
 21
 22 new_add_1 = col(new_add_1)
 23 print(new_add_1(3))

下方是它的返回結果:

new_add
{'args': (3,), 'kwargs': {}, 'func': <function new_add at 0x10d32aa70>, 'inner': <function col.<locals>.inner at 0x10d32acb0>}
('args', 'kwargs')
('func', 'inner')
5
new_add_1
{'args': (3,), 'kwargs': {}, 'func': <function new_add_1 at 0x10d32add0>, 'inner': <function col.<locals>.inner at 0x10d32a8c0>}
('args', 'kwargs')
('func', 'inner')
6

1-8:是定義的一個簡單裝飾器,

3:列印當被裝飾函式的名字

4:列印inner這個內部函式中的所有變數

5:列印當前inner的區域性變數;

6:則列印自由變數;

11-13:修飾了一個簡單函式

16,22,23:@這個語法糖,背後實現的過程;

​ 也就是說 col(new_add) 返回的是當前的內部函式的記憶體地址,而這個呼叫這個內部函式時會使用自由變數func即col的區域性變數,進而達到裝飾器的目的;

有引數的裝飾器實現

​ 既然無引數的裝飾器即@col ,通過內部函式的方式裝飾基礎函式,那麼我們呼叫有引數的裝飾器 則可以再原本的基礎即函式col再封裝一層函式,使其達到可以通過裝飾器傳引數的目的

1 from functools import wraps
  2
  3
  4 def col(string="hello world"):
  5     def decorate(func):
  6         @wraps(func)
  7         def inner(*args, **kwargs):
  8             print(string)
  9             return func(*args, **kwargs)
 10         return inner
 11     return decorate
 12
 13
 14 @col()
 15 def new_add(x):
 16     return x+2
 17
 18
 19 @col("hello python")
 20 def new_add_1(x):
 21     return x+3
 22
 23
 24 def new_add_2(x):
 25     return x+4
 26
 27
 28 print(new_add(1))
 29 print(new_add_1(1))
 30
 31
 32 new_add_2 = col("hello china")(new_add_2)
 33 print(new_add_2(1))

匯入wrap是為了修復這個裝飾器的名稱, new_add.__name__ 呼叫時指向被裝飾的函式,而不是內部函式,有興趣的小夥伴可以去了解一下;

4-11:實現了一個帶引數的裝飾器,最外層返回的是我們真正的裝飾器;

32-33:則是@這個裝飾器語法糖背後的實現過程

可以發現new_add與new_add_1這兩個函式的裝飾器是兩個不同值,而我們的裝飾器也返回了不同的對應情況

hello world
3
hello python
4
hello china
5

間而言之:裝飾器就是在我們需要新增功能的函式上進而封裝一層,而python的語法糖@背後,幫助我們省略掉了這些賦值的過程;

3:裝飾器何時呼叫

關於裝飾器何時執行,我們分兩種情況討論,一種是當作指令碼執行時,另一種是當作模組被匯入時;

1 registry = []
  2
  3
  4 def register(func):
  5     print(f"running register {func}")
  6     registry.append(func)
  7     return func
  8
  9
 10 @register
 11 def f1():
 12     print('running f1()')
 13
 14
 15 @register
 16 def f2():
 17     print('running f2()')
 18
 19
 20 def f3():
 21     print('running f3()')
 22
 23
 24 def main():
 25     print('running main()')
 26     print('regisry ->', registry)
 27     f1()
 28     f2()
 29     f3()
 30
 31
 32 if __name__ == '__main__':
 33     main()

當作獨立指令碼執行時:

running register <function f1 at 0x103f9dcb0>
running register <function f2 at 0x103f9ddd0>
running main()
regisry -> [<function f1 at 0x103f9dcb0>, <function f2 at 0x103f9ddd0>]
running f1()
running f2()
running f3()

被當作模組匯入時:

>>> import registration
running register <function f1 at 0x1005a2710>
running register <function f2 at 0x1005a2b90>
>>> registration.registry
[<function f1 at 0x1005a2710>, <function f2 at 0x1005a2b90>]

該段程式碼的裝飾器主要功能是:記錄了被裝飾函式的個數,通常是web框架以這種方式把函式註冊到中央註冊器的某處。

總結:可以發現裝飾器無論是作為模組被匯入,還是單獨的指令碼執行,它都是優先執行的;

4:裝飾器的常用模組

之前介紹的function.wraps不用說了,接下來介紹兩種神奇的裝飾器;

1:singledispatch

何為singledispatch ?

就是在不改變函式本身的功能上覆用該函式,達到重複使用函式名的目的,有點類似多型的感覺;可以把整體方案拆分成多個模組,甚至可以為你無法修改的類提供專門的函式。使用@singledispatch 裝飾的普通函式會變成泛函數(generic function);根據第一個引數的型別,以不同方式執行相同操作的一組函式

1 from functools import singledispatch
  2
  3
  4 @singledispatch
  5 def hello(obj):
  6     print(obj)
  7
  8
  9 @hello.register(str)
 10 def _(text):
 11     print("hello world "+text)
 12
 13
 14 @hello.register(int)
 15 def _(n):
 16     print(n)
 17
 18
 19 hello({"what": "say"})
 20 print('*'*30)
 21 hello('dengxuan')
 22 print('*'*30)
 23 hello(123)
{'what': 'say'}
******************************
hello world dengxuan
******************************
123

從該段程式碼中我們可以發現,當使用singledispatch這個裝飾器時,函式hello可以根據不同的引數返回不同的結果。這樣的好處就是極大的減少程式碼中的if/elif/else,並且可以複用函式名稱 _ (下橫線代表沒用),降低了程式碼的耦合度,達到了多型的效果。

2:lru_cache

根據書上原話:

functools.lru_cache 是非常實用的裝飾器,它實現了備忘 (memoization)功能。這是一項優化技術,它把耗時的函式的結果儲存 起來,避免傳入相同的引數時重複計算。LRU 三個字母是“Least Recently Used”的縮寫,表明快取不會無限制增長,一段時間不用的快取 條目會被扔掉。

1 from my_tools.runtime import clock
  2 import functools
  3
  4
  5 @functools.lru_cache()
  6 @clock
  7 def fibonacci(n):
  8     if n < 2:
  9         return n
 10     return fibonacci(n-2)+fibonacci(n-1)
 11
 12
 13 if __name__ == '__main__':
 14     print(fibonacci(6))

第5行:註釋funtools.lru_cache()

返回結果:

[0.00000046] fibonacci(0) -> 0
[0.00000053] fibonacci(1) -> 1
[0.00006782] fibonacci(2) -> 1
[0.00000030] fibonacci(1) -> 1
[0.00000035] fibonacci(0) -> 0
[0.00000037] fibonacci(1) -> 1
[0.00001312] fibonacci(2) -> 1
[0.00002514] fibonacci(3) -> 2
[0.00010535] fibonacci(4) -> 3
[0.00000030] fibonacci(1) -> 1
[0.00000030] fibonacci(0) -> 0
[0.00000037] fibonacci(1) -> 1
[0.00001209] fibonacci(2) -> 1
[0.00002376] fibonacci(3) -> 2
[0.00000028] fibonacci(0) -> 0
[0.00000038] fibonacci(1) -> 1
[0.00001210] fibonacci(2) -> 1
[0.00000028] fibonacci(1) -> 1
[0.00000036] fibonacci(0) -> 0
[0.00000034] fibonacci(1) -> 1
[0.00001281] fibonacci(2) -> 1
[0.00002466] fibonacci(3) -> 2
[0.00004897] fibonacci(4) -> 3
[0.00008414] fibonacci(5) -> 5
[0.00020196] fibonacci(6) -> 8
8

當取消掉第5行註釋時;

[0.00000040] fibonacci(0) -> 0
[0.00000049] fibonacci(1) -> 1
[0.00008032] fibonacci(2) -> 1
[0.00000066] fibonacci(3) -> 2
[0.00009398] fibonacci(4) -> 3
[0.00000063] fibonacci(5) -> 5
[0.00010943] fibonacci(6) -> 8
8

可以發現,lru_cache()這個裝飾器,極大的提高了計算效能;

maxsize 引數指定儲存多少個呼叫的結果。快取滿了之後,舊的結果會被扔掉,騰出空間。為了得到最佳效能,maxsize 應該設為 2 的 冪。typed 引數如果設為 True,把不同引數型別得到的結果分開儲存,即把通常認為相等的浮點數和整數引數(如 1 和 1.0)區分開。順 便說一下,因為 lru_cache 使用字典儲存結果,而且鍵根據呼叫時傳 入的定位引數和關鍵字引數建立,所以被 lru_cache 裝飾的函式,它的所有引數都必須是可雜湊的。

5:多重灌飾器

@d1
@d2
def f():
  print('hello world')
  
###########################

def f():
  print("hello world")

f = d1(d2(f))

上下兩塊程式碼是等效效果;

想要獲取更多Python學習資料可以加
QQ:2955637827私聊
或加Q群630390733
大家一起來學習討論吧!