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

本文的文字及圖片來源於網絡,僅供學習、交流使用,不具備任何商業用途,若有問題請及時聯繫咱們以做處理html

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

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

1:閉包緩存

1)函數嵌套網絡

2)內部函數使用外部函數的變量閉包

3)外部函數的返回值爲內部函數app

​ 接下來看看《流暢的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你們一塊兒來學習討論吧!

相關文章
相關標籤/搜索