Python decorator

一、編寫無參數的decoratorhtml

Python的 decorator 本質上就是一個高階函數,它接收一個函數做爲參數,而後,返回一個新函數。python

使用 decorator 用Python提供的 @ 語法,這樣能夠避免手動編寫 f = decorate(f) 這樣的代碼。ubuntu

def log(f):
    def fn(x):
        print 'call ' + f.__name__ + '()...'
        return f(x)
    return fn

對於階乘函數,@log工做得很好:ruby

@log
def factorial(n):
    return reduce(lambda x,y: x*y, range(1, n+1))
print factorial(10)

結果:app

call factorial()...
3628800

可是,對於參數不是一個的函數,調用將報錯:函數

@log
def add(x, y):
    return x + y
print add(1, 2)

結果:post

Traceback (most recent call last):
  File "test.py", line 15, in <module>
    print add(1,2)
TypeError: fn() takes exactly 1 argument (2 given)

由於 add() 函數須要傳入兩個參數,可是 @log 寫死了只含一個參數的返回函數。url

要讓 @log 自適應任何參數定義的函數,能夠利用Python的 *args 和 **kw,保證任意個數的參數老是能正常調用:spa

def log(f):
    def fn(*args, **kw):
        print 'call ' + f.__name__ + '()...'
        return f(*args, **kw)
    return fn

如今,對於任意函數,@log 都能正常工做。.net

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 編寫一個@performance,它能夠打印出函數調用的時間
import time
 
def performance(f):
     def fn( * args, * * kw):
         t1 = time.time()
         r = f( * args, * * kw)
         t2 = time.time()
         print 'call %s() in %fs' % (f.__name__, (t2 - t1))
         return r
     return fn
 
@performance
def factorial(n):
     return reduce ( lambda x,y: x * y, range ( 1 , n + 1 ))
print factorial( 10 )

補充:python中的魔法參數:*args和**kwargs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def foo(*args, **kwargs):
    print 'args = ' , args
    print 'kwargs = ' , kwargs
    print '---------------------------------------'
if __name__ == '__main__' :
    foo( 1 , 2 , 3 , 4 )
    foo(a= 1 ,b= 2 ,c= 3 )
    foo( 1 , 2 , 3 , 4 , a= 1 ,b= 2 ,c= 3 )
    foo( 'a' , 1 , None, a= 1 , b= '2' , c= 3 )
     
 
輸出結果以下:
args = ( 1 , 2 , 3 , 4 )
kwargs = {}
—————————————
args = ()
kwargs = {‘a’: 1 , ‘c’: 3 , ‘b’: 2 }
—————————————
args = ( 1 , 2 , 3 , 4 )
kwargs = {‘a’: 1 , ‘c’: 3 , ‘b’: 2 }
—————————————
args = (‘a’, 1 , None)
kwargs = {‘a’: 1 , ‘c’: 3 , ‘b’: ’ 2 ′}
—————————————
 
能夠看到,這兩個是python中的可變參數。*args表示任何多個無名參數,它是一個tuple;**kwargs表示關鍵字參數,它是一個 dict。而且同時使用*args和**kwargs時,必須*args參數列要在**kwargs前,像foo(a= 1 , b=’ 2 ′, c= 3 , a’, 1 , None, )這樣調用的話,會提示語法錯誤「SyntaxError: non-keyword arg after keyword arg」。
 
還有一個很漂亮的用法,就是建立字典:
 
def kw_dict(**kwargs):
return kwargs
print kw_dict(a= 1 ,b= 2 ,c= 3 ) == { 'a' : 1 , 'b' : 2 , 'c' : 3 }
 
其實python中就帶有dict類,使用dict(a= 1 ,b= 2 ,c= 3 )便可建立一個字典了。

二、編寫帶參數的decorator

考察上一節的 @log 裝飾器:

def log(f):
    def fn(x):
        print 'call ' + f.__name__ + '()...'
        return f(x)
    return fn

發現對於被裝飾的函數,log打印的語句是不能變的(除了函數名)。

若是有的函數很是重要,但願打印出'[INFO] call xxx()...',有的函數不過重要,但願打印出'[DEBUG] call xxx()...',這時,log函數自己就須要傳入'INFO'或'DEBUG'這樣的參數,相似這樣:

@log('DEBUG')
def my_func():
    pass

把上面的定義翻譯成高階函數的調用,就是:

my_func = log('DEBUG')(my_func)

上面的語句看上去仍是比較繞,再展開一下:

log_decorator = log('DEBUG')
my_func = log_decorator(my_func)

上面的語句又至關於:

log_decorator = log('DEBUG')
@log_decorator
def my_func():
    pass

因此,帶參數的log函數首先返回一個decorator函數,再讓這個decorator函數接收my_func並返回新函數:

def log(prefix):
    def log_decorator(f):
        def wrapper(*args, **kw):
            print '[%s] %s()...' % (prefix, f.__name__)
            return f(*args, **kw)
        return wrapper
    return log_decorator

@log('DEBUG')
def test():
    pass
print test()

執行結果:

[DEBUG] test()...
None

 Python Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import time

def performance(unit):
     def perf_decorator(f):
         def wrapper(*args, **kw):
            t1 = time.time()
            r = f(*args, **kw)
            t2 = time.time()
            t = (t2 - t1) *  1000  if unit== 'ms'  else (t2 - t1)
             print  'call %s() in %f %s' % (f.__name__, t, unit)
             return r
         return wrapper
     return perf_decorator

@performance( 'ms'# factorial = performance('ms')(factorial)
def factorial(n):
     return  reduce( lambda x,y: x*y,  range( 1, n+ 1))

print factorial( 10)

# 總結(代碼不能運行)
@performance  # factorial = performance(factorial)
@performance( 'ms'# factorial = performance('ms')(factorial)
def factorial(n):
     pass

三、完善decorator

@decorator能夠動態實現函數功能的增長,可是,通過@decorator「改造」後的函數,和原函數相比,除了功能多一點外,有沒有其它不一樣的地方?

在沒有decorator的狀況下,打印函數名:

def f1(x):
    pass
print f1.__name__

輸出: f1

有decorator的狀況下,再打印函數名:

def log(f):
    def wrapper(*args, **kw):
        print 'call...'
        return f(*args, **kw)
    return wrapper
@log
def f2(x):
    pass
print f2.__name__

輸出: wrapper

可見,因爲decorator返回的新函數函數名已經不是'f2',而是@log內部定義的'wrapper'。這對於那些依賴函數名的代碼就會失效。decorator還改變了函數的__doc__等其它屬性。若是要讓調用者看不出一個函數通過了@decorator的「改造」,就須要把原函數的一些屬性複製到新函數中:

def log(f):
    def wrapper(*args, **kw):
        print 'call...'
        return f(*args, **kw)
    wrapper.__name__ = f.__name__
    wrapper.__doc__ = f.__doc__
    return wrapper

這樣寫decorator很不方便,由於咱們也很難把原函數的全部必要屬性都一個一個複製到新函數上,因此Python內置的functools能夠用來自動化完成這個「複製」的任務:

import functools
def log(f):
    @functools.wraps(f)
    def wrapper(*args, **kw):
        print 'call...'
        return f(*args, **kw)
    return wrapper

最後須要指出,因爲咱們把原函數簽名改爲了(*args, **kw),所以,沒法得到原函數的原始參數信息。即使咱們採用固定參數來裝飾只有一個參數的函數:

def log(f):
    @functools.wraps(f)
    def wrapper(x):
        print 'call...'
        return f(x)
    return wrapper

也可能改變原函數的參數名,由於新函數的參數名始終是 'x',原函數定義的參數名不必定叫 'x'。

 Python Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 帶參數的@decorator,@functools.wraps的放置
import time, functools

def performance(unit):
     def perf_decorator(f):
        @functools.wraps(f)
         def wrapper(*args, **kw):
             return f(*args, **kw)
         return wrapper
     return perf_decorator

@performance( 'ms')
def factorial(n):
     return  reduce( lambda x,y: x*y,  range( 1, n+ 1))

print factorial.__name__

四、偏函數

當一個函數有不少參數時,調用者就須要提供多個參數。若是減小參數個數,就能夠簡化調用者的負擔。

好比,int()函數能夠把字符串轉換爲整數,當僅傳入字符串時,int()函數默認按十進制轉換:

>>> int('12345')
12345

但int()函數還提供額外的base參數,默認值爲10。若是傳入base參數,就能夠作 進制的轉換:

>>> int('12345', base=8)
5349
>>> int('12345', 16)
74565

假設要轉換大量的二進制字符串,每次都傳入int(x, base=2)很是麻煩,因而,咱們想到,能夠定義一個int2()的函數,默認把base=2傳進去:

def int2(x, base=2):
    return int(x, base)

這樣,咱們轉換二進制就很是方便了:

>>> int2('1000000')
64
>>> int2('1010101')
85

functools.partial就是幫助咱們建立一個偏函數的,不須要咱們本身定義int2(),能夠直接使用下面的代碼建立一個新的函數int2:

>>> import functools
>>> int2 = functools.partial(int, base=2)
>>> int2('1000000')
64
>>> int2('1010101')
85

因此,functools.partial能夠把一個參數多的函數變成一個參數少的新函數,少的參數須要在建立時指定默認值,這樣,新函數調用的難度就下降了。


摘自:慕課網



相關文章
相關標籤/搜索