裝飾器

11.decorator裝飾器

動態的函數增加功能


方法一:給每個函數添加printlog

方法二:通過高階函數返回新函數



方法三:內置@語法


12.編寫無參數decorator

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

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

考察一個@log的定義:

def log(f):

    def fn(x):

        print 'call ' + f.__name__ + '()...'

        return f(x)

    return fn

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

@log

deffactorial(n):

    return reduce(lambda x,y: x*y, range(1,n+1))

printfactorial(10)

結果:

callfactorial()...

3628800

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

@log

def add(x, y):

    return x + y

print add(1, 2)

結果:

Traceback (mostrecent call last):

  File "test.py", line 15, in<module>

    print add(1,2)

TypeError: fn()takes exactly 1 argument (2 given)

因爲 add() 函數需要傳入兩個參數,但是 @log 寫死了只含一個參數的返回函數。

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

def log(f):

    def fn(*args, **kw):

        print 'call ' + f.__name__ + '()...'

        return f(*args, **kw)

    return fn

現在,對於任意函數,@log 都能正常工作。

任務

請編寫一個@performance,它可以打印出函數調用的時間。

13.編寫帶參數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並返回新函數:

deflog(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

對於這種3層嵌套的decorator定義,你可以先把它拆開:

# 標準decorator:

deflog_decorator(f):

    def wrapper(*args, **kw):

        print '[%s] %s()...' % (prefix,f.__name__)

        return f(*args, **kw)

    return wrapper

returnlog_decorator

 

# 返回decorator:

deflog(prefix):

    return log_decorator(f)

拆開以後會發現,調用會失敗,因爲在3層嵌套的decorator定義中,最內層的wrapper引用了最外層的參數prefix,所以,把一個閉包拆成普通的函數調用會比較困難。不支持閉包的編程語言要實現同樣的功能就需要更多的代碼。

任務

上一節的@performance只能打印秒,請給 @performace 增加一個參數,允許傳入's''ms'

@performance('ms')

deffactorial(n):

    return reduce(lambda x,y: x*y, range(1,n+1))

14.完善decorator

@decorator可以動態實現函數功能的增加,但是,經過@decorator改造後的函數,和原函數相比,除了功能多一點外,有沒有其它不同的地方?

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

def f1(x):

    pass

printf1.__name__

輸出: f1

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

def log(f):

    def wrapper(*args, **kw):

        print 'call...'

        return f(*args, **kw)

    return wrapper

@log

def f2(x):

    pass

printf2.__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可以用來自動化完成這個複製的任務:

importfunctools

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'

任務

請思考帶參數的@decorator@functools.wraps應該放置在哪:

defperformance(unit):

    def perf_decorator(f):

        def wrapper(*args, **kw):

            ???

        return wrapper

    return perf_decorator

注意@functools.wraps應該作用在返回的新函數上


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章