1. 程式人生 > >Lesson 029 —— python 修飾器

Lesson 029 —— python 修飾器

Lesson 029 —— python 修飾器

本質就是函式,功能是為其它函式新增附加功能。

原則:

  1. 不修改被修飾函式的原始碼
  2. 不修改被修飾函式的呼叫方式

裝飾器 = 高階函式 + 函式巢狀 + 閉包

高階函式

高階函式英文叫Higher-order function。什麼是高階函式?

以Python內建的求絕對值的函式abs()為例,abs(-10)是函式呼叫,而abs是函式本身。函式本身也可以賦值給變數,即:變數可以指向函式。變數f現在已經指向了abs函式本身。直接呼叫abs()函式和呼叫變數f()完全相同。

>>> f = abs
>>> f
<built-in function abs>

>>> f = abs
>>> f(-10)
10

那麼函式名是什麼呢?函式名其實就是指向函式的變數!對於abs()這個函式,完全可以把函式名abs看成變數,它指向一個可以計算絕對值的函式!

>>> abs = 10
>>> abs(-10)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'int' object is not callable

abs指向10後,就無法通過abs(-10)呼叫該函數了!因為abs這個變數已經不指向求絕對值函式而是指向一個整數10

當然實際程式碼絕對不能這麼寫,這裡是為了說明函式名也是變數。要恢復abs

函式,請重啟Python互動環境。

注:由於abs函式實際上是定義在import builtins模組中的,所以要讓修改abs變數的指向在其它模組也生效,要用import builtins; builtins.abs = 10

既然變數可以指向函式,函式的引數能接收變數,那麼一個函式就可以接收另一個函式作為引數,這種函式就稱之為高階函式。

函式巢狀

在函式中定義另一個函式稱為巢狀函式。巢狀函式可以訪問包圍範圍內的變數。

在Python中,這些非區域性變數只能在預設情況下讀取,我們必須將它們顯式地宣告為非區域性變數(使用nonlocal關鍵字)才能進行修改。

# This is the outer enclosing function
def outer(msg):
    # This is the nested function
    def inner():
        print('inner')
        print(msg)
   print('outer')
   inner()

outer(‘hello’)
inner(‘hello’)    # 此句會出錯

函式有可見範圍,這就是作用域的概念

內部函式不能被外部直接使用,會拋NameError異常

可以看到巢狀函式inner()能夠訪問封閉函式的非區域性變數msg

閉包

閉包是由函式及其相關的引用環境組合而成的實體(即:閉包=函式+引用環境)

python中的閉包從表現形式上定義(解釋)為:如果在一個內部函式裡,對在外部作用域(但不是在全域性作用域)的變數進行引用,那麼內部函式就被認為是閉包(closure).

  • 必須有一個巢狀函式(函式內部的函式)。
  • 巢狀函式必須引用封閉函式中定義的值。
  • 閉包函式必須返回巢狀函式。
>>>def addx(x):  
>>>    def adder(y): return x + y  
>>>    return adder  
>>> c =  addx(8)  
>>> type(c)  
<type 'function'>  
>>> c.__name__  
'adder'  
>>> c(10)  
18  

結合這段簡單的程式碼和定義來說明閉包:

  • 如果在一個內部函式裡:adder(y)就是這個內部函式,
  • 對在外部作用域(但不是在全域性作用域)的變數進行引用:x就是被引用的變數,x在外部作用域addx裡面,但不在全域性作用域裡,
  • 則這個內部函式adder就是一個閉包。

再稍微講究一點的解釋是,閉包=函式塊+定義函式時的環境,adder就是函式塊,x就是環境,當然這個環境可以有很多,不止一個簡單的x。

注意事項:

  1. 閉包中是不能修改外部作用域的區域性變數的

    >>> def foo():  
    ...     m = 0  
    ...     def foo1():  
    ...         m = 1  
    ...         print m  
    ...  
    ...     print m  
    ...     foo1()  
    ...     print m  
    ...  
    >>> foo()  
    0  
    1  
    0  

    從執行結果可以看出,雖然在閉包裡面也定義了一個變數m,但是其不會改變外部函式中的區域性變數m。

    def foo():  
        a = 1  
        def bar():  
            a = a + 1  
            return a  
        return bar  
    
    # 結果
    >>> c = foo()  
    >>> print c()  
    Traceback (most recent call last):  
      File "<stdin>", line 1, in <module>  
      File "<stdin>", line 4, in bar  
    UnboundLocalError: local variable 'a' referenced before assignment  

    這是因為在執行程式碼 c = foo()時,python會匯入全部的閉包函式體bar()來分析其的區域性變數,python規則指定所有在賦值語句左面的變數都是區域性變數,則在閉包bar()中,變數a在賦值符號"="的左面,被python認為是bar()中的區域性變數。再接下來執行print c()時,程式執行至a = a + 1時,因為先前已經把a歸為bar()中的區域性變數,所以python會在bar()中去找在賦值語句右面的a的值,結果找不到,就會報錯。解決的方法很簡單

    def foo():  
        a = [1]  
        def bar():  
            a[0] = a[0] + 1  
            return a[0]  
        return bar  

    只要將a設定為一個容器就可以了。這樣使用起來多少有點不爽,所以在python3以後,在a = a + 1 之前,使用語句nonloacal a就可以了,該語句顯式的指定a不是閉包的區域性變數。

  2. 還有一個容易產生錯誤的事例也經常被人在介紹python閉包時提起,我一直都沒覺得這個錯誤和閉包有什麼太大的關係,但是它倒是的確是在python函數語言程式設計是容易犯的一個錯誤,我在這裡也不妨介紹一下。先看下面這段程式碼

    for i in range(3):  
        print i  

    在程式裡面經常會出現這類的迴圈語句,Python的問題就在於,當迴圈結束以後,迴圈體中的臨時變數i不會銷燬,而是繼續存在於執行環境中。還有一個python的現象是,python的函式只有在執行時,才會去找函式體裡的變數的值。

    flist = []  
    for i in range(3):  
        def foo(x): print x + i  
        flist.append(foo)  
    for f in flist:  
        f(2)  

    可能有些人認為這段程式碼的執行結果應該是2,3,4.但是實際的結果是4,4,4。這是因為當把函式加入flist列表裡時,python還沒有給i賦值,只有當執行時,再去找i的值是什麼,這時在第一個for迴圈結束以後,i的值是2,所以以上程式碼的執行結果是4,4,4.
    解決方法也很簡單,改寫一下函式的定義就可以了。

    for i in range(3):  
        def foo(x,y=i): print x + y  
        flist.append(foo)  

作用:

  1. 當閉包執行完後,仍然能夠保持住當前的執行環境。(當函式中有東西外邊還有引用指向它的時候,它並不會立即回收,而是儲存了這個函式的空間)

    比如說,如果你希望函式的每次執行結果,都是基於這個函式上次的執行結果。我以一個類似棋盤遊戲的例子來說明。假設棋盤大小為50*50,左上角為座標系原點(0,0),我需要一個函式,接收2個引數,分別為方向(direction),步長(step),該函式控制棋子的運動。棋子運動的新的座標除了依賴於方向和步長以外,當然還要根據原來所處的座標點,用閉包就可以保持住這個棋子原來所處的座標。

    origin = [0, 0]  # 座標系統原點  
    legal_x = [0, 50]  # x軸方向的合法座標  
    legal_y = [0, 50]  # y軸方向的合法座標  
    def create(pos=origin):  
        def player(direction,step):  
            # 這裡應該首先判斷引數direction,step的合法性,比如direction不能斜著走,step不能為負等  
            # 然後還要對新生成的x,y座標的合法性進行判斷處理,這裡主要是想介紹閉包,就不詳細寫了。  
            new_x = pos[0] + direction[0]*step  
            new_y = pos[1] + direction[1]*step  
            pos[0] = new_x  
            pos[1] = new_y  
            #注意!此處不能寫成 pos = [new_x, new_y],原因在上文有說過  
            return pos  
        return player  
    
    player = create()  # 建立棋子player,起點為原點  
    print(player([1,0],10))  # 向x軸正方向移動10步  
    print(player([0,1],20))  # 向y軸正方向移動20步  
    print(player([-1,0],10))  # 向x軸負方向移動10步

    輸出為

    [10, 0]  
    [10, 20]  
    [0, 20]  
  2. 閉包可以根據外部作用域的區域性變數來得到不同的結果,這有點像一種類似配置功能的作用,我們可以修改外部的變數,閉包根據這個變數展現出不同的功能。比如有時我們需要對某些檔案的特殊行進行分析,先要提取出這些特殊行。

    def make_filter(keep):  
        def the_filter(file_name):  
            file = open(file_name)  
            lines = file.readlines()  
            file.close()  
            filter_doc = [i for i in lines if keep in i]  
            return filter_doc  
        return the_filter  

    如果我們需要取得檔案"result.txt"中含有"pass"關鍵字的行,則可以這樣使用例子程式

    filter = make_filter("pass")  
    filter_result = filter("result.txt")  

    以上兩種使用場景,用面向物件也是可以很簡單的實現的,但是在用Python進行函數語言程式設計時,閉包對資料的持久化以及按配置產生不同的功能,是很有幫助的。

裝飾器(Decorator)

裝飾器本質上是一個Python函式,它可以讓其他函式在不需要做任何程式碼變動的前提下增加額外功能.裝飾器的作用就是為已經存在的物件新增額外的功能。

實際上,實現特殊方法__call__()的任何物件都被稱為可呼叫。 因此,在最基本的意義上,裝飾器是可呼叫的,並且可以返回可呼叫。基本上,裝飾器接收一個函式,新增一些函式並返回,這個新的函式一般不會修改被修飾函式的返回結果。

def make_pretty(func):
    def inner():
        print("I got decorated")
        func()
    return inner

def ordinary():
    print("I am ordinary")

在 shell 中執行

>>> ordinary()
I am ordinary

>>> # let's decorate this ordinary function
>>> pretty = make_pretty(ordinary)
>>> pretty()
I got decorated
I am ordinary

在上面的例子中,make_pretty()是一個裝飾器。 在分配步驟。

pretty = make_pretty(ordinary)

函式ordinary()得到了裝飾,返回函式的名字:pretty

可以看到裝飾函式為原始函式添加了一些新功能。這類似於包裝禮物。 裝飾器作為包裝紙。 裝飾物品的性質(裡面的實際禮物)不會改變。 但現在看起來很漂亮(因為裝飾了)。

一般來說,我們裝飾一個函式並重新分配它,

ordinary = make_pretty(ordinary)

這是一個常見的結構,Python有一個簡化的語法。

可以使用@符號和裝飾器函式的名稱,並將其放在要裝飾的函式的定義之上。 例如,

@make_pretty
def ordinary():
    print("I am ordinary")

上面程式碼相當於:

def ordinary():
    print("I am ordinary")
ordinary = make_pretty(ordinary)

注:如果 make_pretty 不加引數的話,比如直接 def make_pretty(),這樣定義,他是會報錯的

@make_pretty
TypeError: make_pretty() takes 0 positional arguments but 1 was given

裝飾器執行時機

在模組載入時就會執行修飾器函式, 生成一個個被修飾的新的函式. 這兩點可簡單驗證.

# 模組 test.py
def w1(func):
    print("---正在裝飾1----")
    def inner():
        print("---1111111111----")
        func()
        print("---1111111111----")
    return inner

@w1
def f1():
    print("---f1---")

if __name__ == "__main__":
    print("f1: ", f1)
    print("w1: ", w1)
    print("執行 f1()")
    print(f1())

執行指令碼:

>>> import test
---正在裝飾1----

# 或者
python -c 'import test'
# 結果
---正在裝飾1----

# 執行 test.py
python test.py
# 結果
---正在裝飾1----
f1:  <function w1.<locals>.inner at 0x7fad1bc22bf8>
w1:  <function w1 at 0x7fad1bd091e0>
執行 f1()
---1111111111----
---f1---
---1111111111----
None

可以看出,裝飾器 @w1 這一行,其實在函式沒有被呼叫之前,即匯入或者說載入的時候已經執行了, 這一句就等於 f1=w1(f1) 所以 w1 函式已經被呼叫了,返回的是inner()函式的引用,

所以說如果再呼叫 f1() ,其實執行的是 inner() ,而真正的 f1() 函式的引用現在正被儲存在 w1() 函式中的 func引數裡面。

被修飾後的函式已經不是原來的函數了.實際上, f1=w1(f1). 這樣帶來的壞處時遮蔽了原函式的元資訊, 例如__name__, __doc__. 如果要保留原函式的資訊, 可使用標準庫中的修飾器functools.wraps():

import functools

def w1(func):
    print("---正在裝飾1----")
    @functools.wraps(func)
    def inner():
        print("---1111111111----")
        func()
        print("---1111111111----")
    return inner

@w1
def f1():
    print("---f1---")

if __name__ == "__main__":
    print("f1: ", f1)
    print("w1: ", w1)
    print("執行 f1()")
    print(f1())

# 結果
---正在裝飾1----
f1:  <function f1 at 0x7fed93231620>
w1:  <function w1 at 0x7fed932ef1e0>
執行 f1()
---1111111111----
---f1---
---1111111111----
None

兩層裝飾(連結裝飾器)

多個裝飾器可以在Python中連結。

這就是說,一個函式可以用不同(或相同)裝飾器多次裝飾。只需將裝飾器放置在所需函式之上。

def w1(func):
    print("---正在裝飾1----")
    def inner():
        print("---1111111111----")
        func()
        print("---1111111111----")
    return inner

def w2(func):
    print("---正在裝飾2----")
    def inner():
        print("---2222222222----")
        func()
        print("---2222222222----")
    return inner

@w1
@w2
def f1():
    print("---f1---")

if __name__ == "__main__":
    print("f1: ", f1)
    print("w2: ", w2)
    print("w1: ", w1)
    print("執行 f1()")
    print(f1())

從執行結果可以看出,首先呼叫裝飾器 w2,再呼叫裝飾器 w1,也就是說 執行到 @w1 這一行,因為在它下面的並不是一個函式,所以 w1 先暫停,先呼叫 w2, w2 裝飾完成之後,返回的是w2 的 inner 函式的引用, w1 再開始對 w2inner 函式進行裝飾. 最後返回的是w1 的 inner 函式.如果最後呼叫 f1() 那麼執行結果為:

---正在裝飾2----
---正在裝飾1----
f1:  <function w1.<locals>.inner at 0x7fd6af354d08>
w2:  <function w2 at 0x7fd6af354b70>
w1:  <function w1 at 0x7fd6af43b1e0>
執行 f1()
---1111111111----
---2222222222----
---f1---
---2222222222----
---1111111111----
None

實際的執行過程大概如下:

  1. 執行到 @w1,它下面不是一個函式,w1 先暫停。

  2. 繼續執行下一行 @w2,符合條件,則執行 w2(),相當於 f1 = w2(f1)w2 返回 w2 中的 inner函式的引用,則可以看成 f1 = w2.inner

  3. @w2 執行完之後,檢查 @w1,下面是函式,即上面的 f1,所以繼續執行第一步暫停的 w1,即 f1 = w1(f1) = w1(w2.inner)w1 返回 w1 函式中的巢狀函式 inner的引用,則結果可以看成是 f1 = w1.inner(w2.inner(f1))(實際上 inner 函式沒有傳入引數,是以閉包的性質傳入的引數,即引數是從函式 w1, w2 傳入的),則 f1w1ininer 函式的引用。

  4. 執行 f1 實際上就相當於執行 w1.inner(w2.inner(f1)),則按順序執行;

    1, 先列印 ---1111111111----
    2. 然後執行傳入的引數,即傳入的函式;由於傳入的引數是 w2.inner,即 w2 函式中 innner 函式的引用
     1. 執行此函式(執行函式可以看成是直接把函式中的語句複製過來替換掉原來執行函式的語句)則打     印 ---2222222222----
        2. 然後執行傳入的引數,即傳入的函式;由於傳入的引數是未修飾的 f1
         1. 即執行 f1,打印出 ---f1---
            2. 返回上一級函式,即 w2.inner
        3. 繼續執行 w2.innner 函式語句,則列印:---2222222222----
        4. 執行完畢,返回上一級函式,即 w1.inner
    3. 繼續執行 w1.inner函式,即修飾之後的 f1 函式,列印: ---1111111111----
    4. 函式執行完畢,返回主程式

實際上,裝飾其實也即是將裝飾函式與原函式進行重新構造,形成新的函式的過程,是在匯入模組的時候就執行了的。構造完成的函式,及其變數,只有在執行到的時候才會定址呼叫。

裝飾有引數的函式

上面的裝飾器很簡單,只適用於沒有任何引數的函式。 如果有函式要接受如下的引數怎麼辦?

# import functools

def w1(func):
    print("---正在裝飾1----")
    # @functools.wraps(func)
    # def inner(*args, **kwargs)   # 可以接受任何引數
    def inner(x, y):
        print("---1111111111----")
        # sum = func(*args, **kwargs)
        sum = func(x, y)
        print("---1111111111----")
        return sum
    return inner

@w1
def f1(a, b):
    print("---f1---")
    return a+b

if __name__ == "__main__":
    print("f1: ", f1)
    print("w1: ", w1)
    print("執行 f1()")
    print(f1(2, 3))

# 結果
---正在裝飾1----
f1:  <function w1.<locals>.inner at 0x7f83a3417bf8>
w1:  <function w1 at 0x7f83a34fe1e0>
執行 f1()
---1111111111----
---f1---
---1111111111----
5

以這種方式就可以裝飾函式的引數了。

應該會注意到,裝飾器中巢狀的inner()函式的引數與其裝飾的函式的引數是一樣的。 考慮到這一點,現在可以讓一般裝飾器使用任何數量的引數。

那麼,如果被裝飾的函式有返回值,同樣,在 inner() 裡面把函式返回的東西用個變數儲存起來,然後 在 inner() 裡面 return 即可。

在Python中,這個由function(* args,** kwargs)完成。 這樣,args將是位置引數的元組,kwargs將是關鍵字引數的字典。

若我們在 inner() 中返回傳入的函式的值,會發生什麼呢?

# import functools

def w1(func):
    print("---正在裝飾1----")
    # @functools.wraps(func)
    def inner(x, y):
        print("---1111111111----")
        print("w1.innner")
        print("---1111111111----")
        return func(x,y)
    return inner

@w1
def f1(a, b):
    print("---f1---")
    return a+b

if __name__ == "__main__":
    print("f1: ", f1)
    print("w1: ", w1)
    print("執行 f1()")
    print(f1(2, 3))

# 結果
---正在裝飾1----
f1:  <function w1.<locals>.inner at 0x7f783cb0cbf8>
w1:  <function w1 at 0x7f783cbf31e0>
執行 f1()
---1111111111----
w1.innner
---1111111111----
---f1---
5

我們可以看到,內嵌函式返回未被裝飾的函式,也就是原函式,裝飾後的函式仍然是 w1.inner 函式的引用,並沒有改變修飾之後的函式引用。

裝飾器帶引數(裝飾器加括號)

若我們給裝飾器之後加括號,或者再傳入引數,然後再改一改,會發生什麼呢?

# 模組 test.py
def w1():
    print("---正在裝飾1----")
    def inner(func):
        print("---1111111111----")
        print("w1.inner")
        print("---1111111111----")
    return inner

@w1()
def f1():
    print("---f1---")

if __name__ == "__main__":
    print("f1: ", f1)
    print("w1: ", w1)
    print("執行 f1()")
    print(f1())

# 匯入模組 import test.py
---正在裝飾1----
---1111111111----
w1.inner
---1111111111----

# 結果
---正在裝飾1----
---1111111111----
w1.inner
---1111111111----
f1:  None
w1:  <function w1 at 0x7f16c606a1e0>
執行 f1()
Traceback (most recent call last):
  File "test.py", line 17, in <module>
    print(f1())
TypeError: 'NoneType' object is not callable

可以看出,模組匯入的時候竟然連裡面的 inner 函式也被執行了一遍,因為輸出了 ---111111111111-----,這說明,如果 @w1() 這樣用 ,那麼它首先會把 w1() 函式執行一遍 , 這個時候返回的是 inner 函式的引用,那麼 @w1() 就變成了 @inner 這個時候再把 f1 傳到了inner 函式裡面開始進行裝飾(即執行 inner 函式),所以 inner 函式被執行,由於 inner 函式的返回值是 None,則 f1 最終的指向是 None

注: 區分清楚函式的引用與函式的呼叫。函式名是函式的地址,即函式的引用,是變數;函式名後面加括號是函式的呼叫,即執行函式。裝飾器的 @ 後面跟的是函式的引用。

利用這個特點,可以在裝飾器中帶有引數 ,只不過為了防止呼叫,需要在外面再加上一層:

import functools

def a(flag=False):
    print("---正在裝飾 a----")
    if flag:
        def w1(func):
            print("---正在裝飾1----")
            @functools.wraps(func)
            def inner():
                print("---1111111111----")
                print("flag: ", flag)
                func()
                print("---1111111111----")
            return inner
        return w1
    else:
        def w2(func):
            print("---正在裝飾2----")
            def inner():
                print("---2222222222----")
                print("flag: ", flag)
                func()
                print("---2222222222----")
            return inner
        return w2

新增下面裝飾器,然後執行指令碼的結果為:

@a(False)   # 必須要傳入引數
def f1():
    print("---f1---")

if __name__ == "__main__":
    print("f1: ", f1)
    print("執行 f1()")
    print(f1())
    
# 匯入模組 python -c 'import he' 
---正在裝飾 a----
---正在裝飾2----

# 執行結果
---正在裝飾 a----
---正在裝飾2----
f1:  <function a.<locals>.w2.<locals>.inner at 0x7f1f3a4ff730>
執行 f1()
---2222222222----
flag:  False
---f1---
---2222222222----
None

可以看到裝飾後 f1a.w2.inner 的引用。過程如下:

  1. 先執行 a1(False)a1 裡面用 flag 這個變數儲存傳遞的引數,返回的是 w2 的引用
  2. 裝飾器那一行 變成了 @w2 ,然後把 f1 傳遞進去,呼叫 w2 開始進行裝飾
  3. 裝飾完成後 返回的 是 inner 的引用 所以 現在 f1 = inner

我們將裝飾器的引數換一下使用:

@a(True)
def f1():
    print("---f1---")

if __name__ == "__main__":
    print("f1: ", f1)
    print("執行 f1()")
    print(f1())

# 匯入模組
---正在裝飾 a----
---正在裝飾1----

# 執行指令碼
---正在裝飾 a----
---正在裝飾1----
f1:  <function f1 at 0x7f6f258986a8>
執行 f1()
---1111111111----
flag:  True
---f1---
---1111111111----
None

正好驗證了我們之前的道理。@a() 會先執行函式的呼叫,即a(),然後才會執行裝飾器,即@ 進行裝飾函式。