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

1、預備知識

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

函數即變量

函數本質上也是一種變量,函數名即變量名,函數體就變量對應的值;函數體能夠做爲值賦給其餘變量(函數),也能夠經過函數名來直接調用函數。調用符號即()。segmentfault

嵌套函數

函數內部能夠嵌套定義一層或多層函數,被嵌套的內部函數能夠在外層函數體內部調用,也能夠做爲返回值直接返回設計模式

閉包

在一個嵌套函數中,內部被嵌套的函數能夠調用外部函數非全局變量而且不受外部函數聲明週期的影響(便可以把外部函數非全局變量視爲全局變量直接調用)。閉包

高階函數

把一個函數做爲參數傳遞給另一個函數,而且把函數名做爲返回值以便調用函數。app

Python中變量賦值及調用機制

python中變量定義的過程以下:函數

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

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

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

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

這一機制可經過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>

2、裝飾器的需求背景

設想這樣一個現實場景:本身開發的應用在線上穩定運行了一年,後面隨着業務的發展發現原有的經過某些函數定義的部分功能須要擴展一下新功能,剛好現有的功能又做爲公共接口在不少地方被調用。

可能的實現方式:

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

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

2. 把新功能封裝成可接收函數做爲參數、同時調用原函數的高階函數,而後經過嵌套函數來調用返回高階函數
    這就須要用到今天的主角裝飾器了。

3、裝飾器的定義

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

4、裝飾器的做用和應用場景

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

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

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

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

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

6、裝飾器的本質

先來逐個梳理如下要點:

  • 裝飾器如何裝飾其餘函數?
    經過高階函數的特性,把被裝飾的函數做爲參數傳遞到裝飾器內部,而後在裝飾器內部嵌套定義一個專門用於裝飾的函數,該函數在實現對被裝飾函數的調用執行的同時,封裝實現須要添加的額外功能。
  • 裝飾器如何實現不改變對被裝飾函數的調用形式?
    在裝飾器內部調用被裝飾的函數時,就像未引入裝飾器概念同樣簡簡單單地調用被裝飾的函數便可。
  • 爲何講裝飾器會返回一個替換函數?
    裝飾器自己也是一個函數,雖然它已經調用了被裝飾的函數,且封裝實現了須要添加的額外功能,但咱們要使用它也須要像普通函數同樣去調用執行才行。此外,調用執行後要達到咱們的預期目的,裝飾器的返回值須要包含對被裝飾函數的調用執行和額外添加功能的實現。預備知識已經闡述過,對變量的調用時經過對變量名保存的內存空間地址的引用來實現的,所以這裏能夠直接返回裝飾器的函數名(內存空間地址),以便後續在須要的地方直接經過調用符號()來調用實現。
    當咱們把須要被裝飾的函數傳遞給裝飾器後,被裝飾的函數本質上發生了革命性的變化,即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回去以便後續調用裝飾;內層完成對被裝飾的函數的原生態調用和自定義的額外裝飾功能;
    裝飾器返回的替換函數的本質在於嵌套函數的內層不只實現了對被裝飾函數的原生態調用,且額外增長了預期裝飾的功能,調用裝飾器的過程當中在不改變被裝飾函數名稱(變量名)的前提下,改變了函數體(變量的值);
    裝飾器自己只能接收被裝飾的函數做爲惟一的參數,可是能夠在內層函數中定義額外參數來實現對帶參數的函數進行裝飾的目的,同時還能夠再在外層增長一層嵌套,爲裝飾器定義其它的參數(具體在下文程序示例中會演示);
    裝飾器的做用所有體如今裝飾二字上。

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

  • 裝飾無參數的函數
    來一個再普通不一樣的栗子吧:
      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   # 程序輸出結果符合預期

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

相關文章
相關標籤/搜索