Python學習之路day4-函數高級特性、裝飾器


一、預備知識

學習裝飾器需理解以下預備知識:

函數即變量

函數本質上也是一種變量,函數名即變量名,函數體就變量對應的值;函數體可以作為值賦給其他變量(函數),也可以通過函數名來直接調用函數。調用符號即()。

嵌套函數

函數內部可以嵌套定義一層或多層函數,被嵌套的內部函數可以在外層函數體內部調用,也可以作為返回值直接返回

閉包

在一個嵌套函數中,內部被嵌套的函數可以調用外部函數非全局變量並且不受外部函數聲明周期的影響(即可以把外部函數非全局變量視為全局變量直接調用)。

高階函數

把一個函數作為參數傳遞給另外一個函數,並且把函數名作為返回值以便調用函數。

Python中變量賦值及調用機制

python中變量定義的過程如下:

1. 在內存中分配一塊內存空間;

2. 將變量的值存放到這塊內存空間;

3. 將這塊內存空間的地址(門牌號)賦值給變量名(即變量名保存的是內存空間地址)

總結:變量保存的不是變量對應的真實值,而是真實值所被存放的內存空間地址,這也就意味着變量的調用需要通過調用內存空間地址(門牌號)來實現。將變量延伸到函數,函數和函數的參數都屬於變量,調用函數進行參數傳遞時,是對函數和參數兩個變量的同時調用,符合變量賦值及調用的機制(間接引用而非直接調用變量對應的值)。參數的傳遞實質上是一種引用傳遞,即通過傳遞對實參所在內存空間地址的指向來完成傳遞過程。

這一機制可通過id()來驗證:

  1 list1 = ['Python', 'PHP', 'JAVA']
  2 list2 = list1
  3 print(id(list1), '========', id(list2))
  4 print('')
  5 
  6 
  7 def foo1(x):
  8     print(id(foo1))
  9     print(id(x))
 10     print(foo1)
 11 
 12 
 13 def foo2():
 14     pass
 15 
 16 foo1(foo2())
 17 print('---------')
 18 print(id(foo1))
 19 print(id(foo2()))
 20 print(foo1)
 21 
 22 輸出:
 23 7087176 ======== 7087176  #普通變量調用,內存地址指向相同
 24 
 25 7082192
 26 1348178992
 27 <function foo1 at 0x00000000006C10D0>
 28 ---------  # 函數調用前后,不僅函數名指向的內存地址相同,實參和形參的內存地址也相同
 29 7082192
 30 1348178992
 31 <function foo1 at 0x00000000006C10D0>

二、裝飾器的需求背景

設想這樣一個現實場景:自己開發的應用在線上穩定運行了一年,后面隨着業務的發展發現原有的通過某些函數定義的部分功能需要擴展一下新功能,恰好現有的功能又作為公共接口在很多地方被調用。

可能的實現方式:

1. 調整代碼,重新定義需要修改功能對應的函數

    這需要傷筋動骨了,重點是需要確保代碼的一致性,另外有可能重新定義后原來的函數調用方式沒法裝載新功能。要知道這是在線上穩定運行的系統呀!

2. 把新功能封裝成可接收函數作為參數、同時調用原函數的高階函數,然后通過嵌套函數來調用返回高階函數
    這就需要用到今天的主角裝飾器了。

三、裝飾器的定義

顧名思義,裝飾器是用來裝飾的,它本身也是一個函數,只不過接收其它函數作為參數並裝飾其它函數,為其它函數提供額外的附加功能。最直接的定義是,裝飾器其實就是一個接收函數作為參數,並返回一個替換函數的可執行函數(詳情參照下文論述)。

四、裝飾器的作用和應用場景

上文已經大概提到,裝飾器是裝飾其他函數的,為其他函數提供原本沒有的附加功能。引用一段比較詳細的文字:裝飾器是一個很著名的設計模式,經常被用於有切面需求的場景,較為經典的有插入日志、性能測試、事務處理等。

裝飾器是解決這類問題的絕佳設計,有了裝飾器,我們就可以抽離出大量函數中與函數功能本身無關的雷同代碼並繼續重用。概括的講,裝飾器的作用就是為已經存在的對象添加額外的功能。

五、定義和使用裝飾器的原則

定義和使用裝飾器需遵循以下原則:

  • 不能修改被裝飾的函數的源代碼
  • 不能改變被裝飾函數的調用方式
    以上兩點是為了保障被裝飾函數的一致性和維護性,以及新增功能的擴展性和可重用性(與原函數無關)。

六、裝飾器的本質

先來逐個梳理以下要點:

  • 裝飾器如何裝飾其他函數?
    通過高階函數的特性,把被裝飾的函數作為參數傳遞到裝飾器內部,然后在裝飾器內部嵌套定義一個專門用於裝飾的函數,該函數在實現對被裝飾函數的調用執行的同時,封裝實現需要添加的額外功能。
  • 裝飾器如何實現不改變對被裝飾函數的調用形式?
    在裝飾器內部調用被裝飾的函數時,就像未引入裝飾器概念一樣簡簡單單地調用被裝飾的函數即可。
  • 為什么講裝飾器會返回一個替換函數?
    裝飾器本身也是一個函數,雖然它已經調用了被裝飾的函數,且封裝實現了需要添加的額外功能,但我們要使用它也需要像普通函數一樣去調用執行才行。此外,調用執行后要達到我們的預期目的,裝飾器的返回值需要包含對被裝飾函數的調用執行和額外添加功能的實現。預備知識已經闡述過,對變量的調用時通過對變量名保存的內存空間地址的引用來實現的,因此這里可以直接返回裝飾器的函數名(內存空間地址),以便后續在需要的地方直接通過調用符號()來調用實現。
    當我們把需要被裝飾的函數傳遞給裝飾器后,被裝飾的函數本質上發生了革命性的變化,即foo=wrapper(foo), 雖然與被裝飾之前名稱看着相同,但實質內容是返回的被裝飾后的函數,即返回了一個替換函數。
  • 為什么裝飾器都至少需要雙層嵌套函數呢?
    查詢資料就可以發現講解裝飾器時的程序示例中的裝飾器都至少設計了兩層嵌套函數,外部的那層用於把被裝飾的函數作為參數傳遞進去,內部的那層才是真正的裝飾所用。直接把兩層合二為一,在一個函數內部合並裝飾功能難道就不行嗎?
    舉例驗證一下:
    先來正統版裝飾器吧:
  1 import time
  2 
  3 
  4 def timmer(func):  #外層函數傳遞被裝飾的函數
  5     def warpper(*args,**kwargs):
  6         start_time=time.time()
  7         func() #保持原樣調用被裝飾的函數
  8         stop_time=time.time()
  9         print('the func run time is %s' %(stop_time-start_time))
 10     return warpper #把內層實際調用執行被裝飾的函數以及封裝額外裝飾功能的函數名(內存空間地址)作為返回值返回,以便后續調用執行
 11 
 12 @timmer
 13 def test1():
 14     time.sleep(3)
 15     print('in the test1')
 16 
 17 test1()
 18 
 19 程序輸出:
 20 in the test1    #原生方式調用執行被裝飾的函數
 21 the func run time is 3.000171661376953 #附加了額外的程序執行時間統計功能

      請注意這里定義的warpper函數的參數形式,非固定參數意味着實際傳入的被裝飾的函數可以有參數也可以沒有參數。
      現在我們把上述雙層嵌套函數改裝成一個函數來試試:

  1 import time
  2 def timmer(func):
  3     start_time=time.time()
  4     func()
  5     stop_time=time.time()
  6     print('the func run time is %s' %(stop_time-start_time))
  7     return timmer    #去掉內層嵌套函數warpper,直接返回timmer自身
  8 
  9 
 10 @timmer
 11 
 12 
 13 def test1():
 14     time.sleep(3)
 15     print('in the test1')
 16 test1()
 17 
 18 程序輸出:
 19 in the test1
 20 Traceback (most recent call last):
 21 the func run time is 3.000171661376953
 22   File "E:/Python_Programs/S13/day4/deco.py", line 20, in <module>
 23     test1()
 24 TypeError: timmer() missing 1 required positional argument: 'func'

      請注意我們改編裝飾器后程序雖然可以運行,但已然報錯了,提示最后一行在調用test1這個被裝飾的函數時少了一個位置參數func:改編后的程序的返回值
      是timmer本身,而我們在定義timmer函數時已經為其定義了一個參數func,因此報出缺少參數錯誤。
     
關於這個參數錯誤,我們同樣可以通過修改內層嵌套函數的參數形式來佐證一下:

  1 import time
  2 def timmer(func):
  3     def warpper(x):   #這里故意為內層函數定義一個參數
  4         print(x)
  5         start_time=time.time()
  6         func()
  7         stop_time=time.time()
  8         print('the func run time is %s' %(stop_time-start_time))
  9     return warpper
 10 
 11 @timmer
 12 def test1():
 13     time.sleep(3)
 14     print('in the test1')
 15 
 16 test1()
 17 
 18 程序輸出:
 19 Traceback (most recent call last):
 20   File "E:/Python_Programs/S13/day4/deco2.py", line 18, in <module>
 21     test1()
 22 TypeError: warpper() missing 1 required positional argument: 'x'

      可以看出我們修改內層函數的參數定義后會報相同的錯誤,而且直接導致程序不能運行了!我們第一個演示程序中內層函數的參數是非固定參數,可有可無,因此運行OK。

      還記得上文強調的裝飾器的原則么?一是不改變對被裝飾函數的調用執行方式(就要原生態調用);二是不改變被裝飾函數的源代碼。這改編后的裝飾器的問題就在於不能滿足第一條原生態調用被
      裝飾函數的條件了。要修復這個問題,我們只能返回一個不帶任何參數或者說可以不帶參數的函數作為返回值,而現狀是裝飾器函數本身已經被固化了,必須且只能傳入func一個參數以便將被裝飾
      的函數傳遞給裝飾器,因此我們不得不引入一個不帶參數的內嵌函數,用它來完成需要的裝飾並作為返回值以便后續調用。
      於是裝飾器就變成兩層嵌套函數,外層(第一層)函數負責把需要被裝飾的函數作為參數傳遞到裝飾器內部,並定義整個裝飾器的返回值,內層(第二層)函數負責執行具體的裝飾功能,而外層定
      義的返回值就是內層實際原生態調用被裝飾函數和執行額外裝飾功能的內層嵌套函數。
兩層嵌套分工明確又相得益彰,仔細推敲下這設計模式真是太nb了!
      這也是很多地方說高階函數+嵌套函數=>裝飾器的原因。
      在此也附上網上某大神的解答:
      image

    原文地址:https://segmentfault.com/q/1010000006990592

  • 為什么裝飾器的返回值一定要在外層函數中定義?在內層函數中定義可以嗎?
    先復習下函數的返回值有關概念,如果沒有定義return值,那么函數會返回none,此時函數的type是NoneType。當我們在內嵌函數中定義返回值來替代在外層函數中定義返回值時,外層函數就沒有return值,本身類型變成NoneType,實際調用時程序會報“TypeError: 'NoneType' object is not callable”的錯誤。一個既定的事實是,盡管實際執行裝飾功能的函數是內層函數,但我們在調用時還是調用的外層函數,否則被裝飾的函數又沒法傳遞給裝飾器了!
    OK,到這里了再回顧下裝飾器的幾個要點是不是有種步步驚心、環環相扣、天衣無縫的趕腳?
  • 語法糖@又是個什么東東呢?
    稍微注意一下細節不難發現,裝飾器定義后會通過@decorator的方式來調用,這一聲明往往在被裝飾的函數前面的位置出現。這就是傳說中的語法糖。比如上文中的示例程序,@timmer,完全等價於test1=timmer(test1), 這也是裝飾器會返回一個替換函數的精髓所在。這里的有兩個細節需要注意:
    1. 對test1進行賦值時是通過引用方式傳遞的一個函數名(門牌號,內存空間地址);
    2. 下文還需要通過調用方式才能真正實現對test1的裝飾,調用方法就是test1加上調用符號()了
    但是請注意,語法糖並不是什么高大上的東東,千萬不要以為@有另外的魔力。除了字符輸入少了一些,還有一個額外的好處:這樣看上去更有裝飾器的感覺。

    至此,裝飾器的一些要點已經闡述完畢,是時候對裝飾器作一些總結了。

    裝飾器總結:

    裝飾器本身也是一個可執行函數,只不過是一個高階函數;
    裝飾器通過接收函數作為參數,結合嵌套函數來實現對被裝飾函數的裝飾處理;
    裝飾器的嵌套函數至少應該有兩層,外層接收被裝飾的函數作為參數,傳遞給內層,並將內層函數(替換函數)return回去以便后續調用裝飾;內層完成對被裝飾的函數的原生態調用和自定義的額外裝飾功能;
    裝飾器返回的替換函數的本質在於嵌套函數的內層不僅實現了對被裝飾函數的原生態調用,且額外增加了預期裝飾的功能,調用裝飾器的過程中在不改變被裝飾函數名稱(變量名)的前提下,改變了函數體(變量的值);
    裝飾器本身只能接收被裝飾的函數作為唯一的參數,但是可以在內層函數中定義額外參數來實現對帶參數的函數進行裝飾的目的,同時還可以再在外層增加一層嵌套,為裝飾器定義其它的參數(具體在下文程序示例中會演示);
    裝飾器的作用全部體現在裝飾二字上。

七、裝飾器程序示例(裝飾無參數函數、裝飾有參數函數、裝飾器自帶參數)

  • 裝飾無參數的函數
    來一個再普通不同的栗子吧:
      1 def deco1(func):   #外層函數把被裝飾的函數作為參數引入
      2     def wrapper():
      3         print('Begin----')
      4         func()     #內嵌函數開始調用執行被裝飾的函數,調用方式是原生態的
      5         print('End-----')
      6     return wrapper # 此處返回的函數即為替換函數,包含了對原函數調用執行和增加額外裝飾功能的邏輯,注意這里返回的是函數名(門牌號)
      7 
      8 
      9 @deco1
     10 def test1():
     11     print('This is for test')
     12 
     13 test1()  #這里的test1在執行時會被替換為裝飾器中的wrapper函數,並非原本意義上定義的test1函數了,原本意義上定義的test1函數對應於與wrapper中的func()
    #這里通過調用符號()來調用執行被替換后的test1函數,請注意裝飾器中的返回值是wrapper即替換函數的內存空間地址(門牌號),通過調用符號()即可獲取
    函數體(對變量test1進行賦值處理) 14 15 程序輸出: 16 Begin---- 17 This is for test 18 End-----
    看這個示例是不是覺得裝飾不帶參數的函數比較簡單? 盡管這個裝飾器看着沒有太大的實際意義甚至有點low,但足以演示裝飾器的過程了。
  • 裝飾有參數的函數
    裝飾有參數的函數時,參數需要在裝飾器的內層函數中接收。
    為了直觀演示相關邏輯,直接上一個非固定參數的栗子:
      1 __author__ = 'Beyondi'
      2 #!/usr/bin/env python
      3 #! -*- coding:utf-8 -*-
      4 
      5 
      6 def dec2(func): #外層函數只能處理被裝飾的函數這一個參數
      7     def wrapper(*args, **kwargs):  #被裝飾的函數的參數,一定要在內嵌函數中引入處理
      8         print('Begin to decorate...')
      9         ret = func(*args, **kwargs)
     10         print('Arguments are %s %s' % (args, kwargs))
     11         return ret
     12     return wrapper
     13 
     14 
     15 @dec2
     16 def test2(x, y, z):
     17     print('aaa')
     18     return 2
     19 
     20 test2('a', 'b', z='c')  #實際調用時被裝飾的函數參數傳遞方式不變
     21 
     22 程序輸出:
     23 Begin to decorate...
     24 aaa
     25 Arguments are ('a', 'b') {'z': 'c'}
     26 

    搞定了非固定參數的函數裝飾,固定參數的函數裝飾當然更簡單了。
  • 裝飾器自帶參數
    上面的栗子的關注點都在被裝飾的函數是否帶參數,實際應用中要讓裝飾器的功能更強大全面,往往需要給裝飾器也定義參數,以便執行更復雜靈活的邏輯。以下示例程序就在上述程序基礎上對被裝飾的函數傳遞的實參長度進行判斷處理:
      1 def deco(limit):  #裝飾器自帶的參數需要再定義一個外部函數來引入
      2     def dec2(func): #接收處理被裝飾的函數變量
      3         def wrapper(*args, **kwargs): #處理被裝飾的函數傳遞的參數,邏輯不變
      4             print('Begin to decorate...')
      5             # print(args)
      6             func(*args, **kwargs)
      7             if len(args) >= limit:  #裝飾器自帶的參數開始派上用場
      8                 print('Arguments OK')
      9             else:
     10                 print('Arguemts error')
     11         return wrapper
     12     return dec2
     13 
     14 
     15 @deco(2)  #通過語法糖進行裝飾時,需要把裝飾器自帶的參數傳遞進去,改變這個實參會影響程序最后的輸出結果
     16 def test2(x, y, z):
     17     print('aaa')
     18     return 2
     19 
     20 test2('a', 'b', z='c')
     21 
     22 程序輸出:
     23 Begin to decorate...
     24 aaa
     25 Arguments OK   # 程序輸出結果符合預期

      以上程序表明,給裝飾器本身引入參數可實現更靈活強大的裝飾效果。需要注意的是裝飾器自己的參數一定要在裝飾器的最外層定義引入,此時真正的裝飾器
      就是最里層嵌套的函數了。這也是為什么講裝飾器至少是需要雙層嵌套的高階函數。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM