1. 程式人生 > >Python裝飾器-面向切面的程式設計AOP2

Python裝飾器-面向切面的程式設計AOP2

這幾天花了點時間瞭解了下Python的裝飾器。其實以前在書上也看過有關的內容,不過當時不理解。今天把自己的一點體會寫出來跟大家分享一下。

    一、裝飾器能幹啥?

    正如 AstralWind 在他的部落格中介紹,“裝飾器是一個很著名的設計模式,經常被用於有切面需求的場景,較為經典的有插入日誌、效能測試、事務處理等。裝飾器是解決這類問題的絕佳設計,有了裝飾器,我們就可以抽離出大量函式中與函式功能本身無關的雷同程式碼並繼續重用。概括的講,裝飾器的作用就是為已經存在的物件新增額外的功能。”

    二、如何編寫自己的裝飾器?

    讓我們來編寫一個比較簡單的裝飾器,在Python裡面程式碼看起來會是這樣的:

複製程式碼
 1 #!/usr/bin/env python
 2 
 3 def deco(func):
 4     def wrapper():
 5         print "Wrap start"
 6         func()
 7         print "Wrap end\n"
 8     return wrapper
 9 
10 @deco
11 def foo():
12     print "In foo():"
13 
14 foo()
複製程式碼

    執行起來是這個樣子的:

1 $ python decorate.py
2 Wrap start
3 In foo():
4 Wrap end

    我們可以在“Wrap start”和“Wrap end”裡面做一些自己想要的功能,比如計算執行時間,輸出日誌等等。

    三、為什麼要返回一個函式?

    我第一次看到“return wrapper”這句的時候,就在想為什麼要返回一個函式?
    正好CSDN上的文章是沒返回函式的,讓我們來仔細分析一下程式碼:   

複製程式碼
1 def deco(func):  
2     print func  
3     return func  
4 @deco  
5 def foo():pass  
6 
7 foo() 
複製程式碼

    執行的結果看起來很完美,但是隻是看起來。讓我們把程式碼修改一下:

複製程式碼
 1 >>> def deco(func):
 2 ...     print "In deco"
 3 ...     return func
 4 ...
 5 >>> @deco
 6 ... def foo():
 7 ...     print "In foo"
 8 ...
 9 In deco
10 >>>
複製程式碼

    等等,程式碼裡面還沒有呼叫 foo(),怎麼就print “In deco”了?
    這裡編寫的裝飾器根本就沒有達到我們要求的功能,因為它返回的還是 func 本身,print “In deco” 也只會在初始化的時候執行一次,僅僅一次。
    讓我們回到剛開始的例子,在 deco 裡面返回了一個 wrapper 函式物件。可以試著這麼理解,deco的作用是給 func 進行裝飾,wrapper 就是被裝飾過的func。
    然後我們就可以重複呼叫這個被裝飾過的函式。


    四、怎麼裝飾一個 需要傳引數 的函式?

    現在另一個問題又來了,前面被裝飾的函式都是不帶引數的,那帶引數的函式要怎麼裝飾呢? 
    讓我們先嚐試下前面的辦法:   

複製程式碼
 1 >>> def deco(func):
 2 ...     def wrapper():
 3 ...         print "Wrap start"
 4 ...         func()
 5 ...         print "Wrap end\n"
 6 ...     return wrapper
 7 ...
 8 >>> @deco
 9 ... def foo(x):
10 ...     print "In foo():"
11 ...     print "I have a para: %s" % x
12 ...
13 >>> foo('x')
14 Traceback (most recent call last):
15   File "<stdin>", line 1, in <module>
16 TypeError: wrapper() takes no arguments (1 given)
複製程式碼

    報了個缺少引數的錯誤,那把這個引數帶上:

複製程式碼
 1 >>> def deco(func):
 2 ...     def wrapper(x):
 3 ...         print "Wrap start"
 4 ...         func(x)
 5 ...         print "Wrap end\n"
 6 ...     return wrapper
 7 ...
 8 >>> @deco
 9 ... def foo(x):
10 ...     print "In foo():"
11 ...     print "I have a para: %s" % x
12 ...
13 >>> foo('x')
14 Wrap start
15 In foo():
16 I have a para: x
17 Wrap end
複製程式碼

    現在可以正常傳遞引數了。

    五、怎麼裝飾 引數列表不一樣 的多個函式?

    繼續發散一下,要是想裝飾多個函式,但是這些函式的引數列表變化很大的呢?
    這個時候,就到了使用Python引數魔法的時候了。
    定義函式時:*params:收集其餘的位置引數,返回元組。    **params:收集其餘的關鍵字引數,返回字典。
    呼叫函式時:*params:將元組拆分為位置引數傳入。    **params:將字典拆分為關鍵字引數傳入。

    利用上面的引數魔法後,程式碼看起來會是這樣的:

複製程式碼
 1 #!/usr/bin/env python
 2 
 3 def deco(func):
 4     def wrapper(*args, **kwargs):
 5         print "Wrap start"
 6         func(*args, **kwargs)
 7         print "Wrap end\n"
 8     return wrapper
 9 
10 @deco
11 def foo(x):
12     print "In foo():"
13     print "I have a para: %s" % x
14 
15 @deco
16 def bar(x,y):
17     print "In bar():"
18     print "I have two para: %s and %s" % (x, y)
19 
20 @deco
21 def foo_dict(x,z='dict_para'):
22     print "In foo_dict:"
23     print "I have two para, %s and %s" % (x, z)
24 
25 if __name__ == "__main__":
26     foo('x')
27     bar('x', 'y')
28     foo_dict('x', z='dict_para')
複製程式碼


    執行一下看看效果:

複製程式碼
 1 $ python decorate.py
 2 Wrap start
 3 In foo():
 4 I have a para: x
 5 Wrap end
 6 
 7 Wrap start
 8 In bar():
 9 I have two para: x and y
10 Wrap end
11 
12 Wrap start
13 In foo_dict:
14 I have two para, x and dict_para
15 Wrap end
複製程式碼