python學習筆記 函數裝飾器

函數裝飾器

函數裝飾器用於在源碼中「標記」函數, 以某種方式加強函數的行爲,這是一個強大的功能。python

函數裝飾器是一個可調用對象,其參數是另一個函數,即被裝飾函數。裝飾器可能處理被裝飾函數,而後將其返回,或者將其替換成另外一個函數或可調用對象。數組

函數裝飾器的一個重要特性就是,它們在被裝飾的函數定義以後當即運行。緩存

registry = []

def register(func):
    print('running register(%s)' % func)
    registry.append(func)
    return func

@register
def f1():
    print('running f1()')

@register
def f2():
    print('running f2()')

def f3():
    print('running f3()')

def main():
    print('running main()')
    print('registry ->', registry)
    f1()
    f2()
    f3()

if __name__ == '__main__':
    main()

運行結果:閉包

running register(<function f1 at 0x0000022DC2D23620>)
running register(<function f2 at 0x0000022DC2D236A8>)
running main()
registry -> [<function f1 at 0x0000022DC2D23620>, <function f2 at 0x0000022DC2D236A8>]
running f1()
running f2()
running f3()

能夠看到,被裝飾的 f1() 和 f2() 首先被運行,隨後才運行main()中的語句。app

被裝飾函數運行時,其自己的內容(示例中print語句)並無被執行,而是運行了裝飾器函數中的print語句;這就是裝飾器的做用,替代被裝飾函數,同時裝飾器也能夠調用外界自由變量(registry),從而引出一個重要概念:函數

閉包

實例中registry變量和register函數組合的共同體,被成稱爲閉包。code

該例中有兩個不太尋常的地方:orm

  • 裝飾器函數和被裝飾函數定義在一個模塊之中;通常來講,二者應該定義在不一樣的模塊;
  • register裝飾器返回的函數與經過參數傳入的相同;實際上,大部分裝飾器會在內部定義一個函數,而後將其返回。

實現一個簡單的裝飾器

# clockdeco.py 輸出被裝飾函數的運行時間
import time


def clock(func):
    def clocked(*args):
        t0 = time.perf_counter()
        result = func(*args)
        elapsed = time.perf_counter() - t0
        name = func.__name__
        arg_str = ','.join(repr(arg) for arg in args)
        print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
        return result
    return clocked

簡單運用:對象

# clockdeco_demo.py 
import time
from clockdeco import clock

@clock
def snooze(seconds):
    time.sleep(seconds)

@clock
def factorial(n):
    return 1 if n < 2 else n*factorial(n-1)

def main():
    print('*' * 40, 'Calling snooze(.123)')
    snooze(.123)
    print('*' * 40, 'Calling factorial(6)')
    print('6! =', factorial(6))

if __name__ == '__main__':
    main()

運行結果:ci

**************************************** Calling snooze(.123)
[0.12240868s] snooze(0.123) -> None
**************************************** Calling factorial(6)
[0.00000068s] factorial(1) -> 1
[0.00020317s] factorial(2) -> 2
[0.00039755s] factorial(3) -> 6
[0.00053638s] factorial(4) -> 24
[0.00062375s] factorial(5) -> 120
[0.00067319s] factorial(6) -> 720
6! = 720

運行過程當中,首先輸出裝飾器函數中的內容:

  1. 被裝飾函數運行時間長度;
  2. 函數名稱和實際參數
  3. 計算結果

而後獲得最終的計算結果。可見,裝飾器函數的優先級較高

固然,該實例中的裝飾器具備幾個缺點:

  • 不支持關鍵字參數
  • 遮蓋了被裝飾函數的__name__和__doc__屬性

下面的例子對其作出改進:

# clockdeco2.py
import time
import functools


def clock(func):
    @functools.wraps(func)
    def clocked(*args, **kwargs):
        t0 = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - t0
        name = func.__name__
        arg_lst = []
        if args:
            arg_lst.append(','.join(repr(arg) for arg in args))
        if kwargs:
            pairs = ['%s=%r' % (k, w) for k, w in sorted(kwargs.items())]
            arg_lst.append(', '.join(pairs))
        arg_str = ', '.join(arg_lst)
        print('[%0.8fs] %s(%s) -> %r ' % (elapsed, name, arg_str, result))
        return result
    return clocked

運用:

# clockdeco_demo.py
import time
from clockdeco2 import clock

@clock
def snooze(seconds):
    time.sleep(seconds)

@clock
def factorial(n):
    return 1 if n < 2 else n*factorial(n-1)

def main():
    print('*' * 40, 'Calling snooze(.123)')
    snooze(.123)
    print('*' * 40, 'Calling factorial(6)')
    print('6! =', factorial(6))

if __name__ == '__main__':
    main()

運行結果:

**************************************** Calling snooze(.123)
[0.12328553s] snooze(0.123) -> None
**************************************** Calling factorial(6)
[0.00000000s] factorial(1) -> 1
[0.00000000s] factorial(2) -> 2
[0.00000000s] factorial(3) -> 6
[0.00099683s] factorial(4) -> 24
[0.00099683s] factorial(5) -> 120
[0.00099683s] factorial(6) -> 720
6! = 720

改進後的clockdeco2.py中,使用functools.wraps裝飾器把相關屬性從func複製到clocked中,此外,這個新版本還能正確處理關鍵字參數。functools.wraps是標準庫中的裝飾器,它能夠用於裝飾一個函數,可是對於被裝飾函數自己的功能沒有任何影響,它的功能只是傳遞函數內置參數。

參數化clock裝飾器

# clockdeco_param.py
import time


DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'

def clock(fmt=DEFAULT_FMT):
    def decorate(func):
        def clocked(*_args):
            t0 = time.time()
            _result = func(*_args)
            elapsed = time.time() - t0
            name = func.__name__
            args = ','.join(repr(arg) for arg in _args)
            result = repr(_result)
            print(fmt.format(**locals()))
            return _result
        return clocked
    return decorate
  • def clock 是參數化裝飾器工廠函數
  • decorate(func) 是真正的裝飾器
  • clocked 包裝被裝飾的函數

示例1:

# clockdeco_param_demo.py
import time
from clockdeco_param import clock

@clock()
def snooze(seconds):
    time.sleep(seconds)

for i in range(3):
    snooze(.123)

運行結果:

[0.12367034s] snooze(0.123) -> None
[0.12367010s] snooze(0.123) -> None
[0.12366986s] snooze(0.123) -> None

示例2:

# clockdeco_param_demo.py
import time
from clockdeco_param import clock

@clock('{name}:{elapsed}s')
def snooze(seconds):
    time.sleep(seconds)

for i in range(3):
    snooze(.123)

運行結果:

snooze:0.12366843223571777s
snooze:0.12369871139526367s
snooze:0.12366509437561035s

示例3:

# clockdeco_param_demo.py
import time
from clockdeco_param import clock

@clock('{name}({args}) dt={elapsed:0.3f}s')
def snooze(seconds):
    time.sleep(seconds)

for i in range(3):
    snooze(.123)

運行結果:

snooze(0.123) dt=0.124s
snooze(0.123) dt=0.124s
snooze(0.123) dt=0.124s

分析三個示例能夠看出,當裝飾器clock的參數不一樣時,被裝飾函數運行所得結果也會不一樣。

python中參數化裝飾器的用意在於將更多的參數傳送給裝飾器,由於裝飾器的第一個參數必定是被裝飾函數。

備忘裝飾器 functools.lru_cache

functools.lru_cache和functools.wraps同樣,也是一個python內置裝飾器,它的功能是將耗時的函數結果保存起來,避免傳圖相同的參數形成重複計算,從而節省代碼運行時間。

下面以斐波那契數列寫一個案例:

  1. 使用functools.lru_cache

    import functools
    from clockdeco import clock

    @functools.lru_cache()
    @clock
    def fibonacci(n):

    if n < 2:
        return  n
    return fibonacci(n-2) + fibonacci(n-1)

    if __name__=='__main__':

    print(fibonacci(30))

運行結果:

[0.00000000s] fibonacci(0) -> 0
[0.00000068s] fibonacci(1) -> 1
......
[0.00000271s] fibonacci(29) -> 514229
[0.00542815s] fibonacci(30) -> 832040
832040

屢次運行,計算fibonacci(30)大概耗時0.005秒左右

做爲對比:

import functools
from clockdeco import clock


@clock
def fibonacci(n):
    if n < 2:
        return  n
    return fibonacci(n-2) + fibonacci(n-1)

if __name__=='__main__':
    print(fibonacci(30))

運行結果:

.......
[156.42139917s] fibonacci(28) -> 317811
[230.80184171s] fibonacci(29) -> 514229
[368.52227404s] fibonacci(30) -> 832040
832040

嗯……陷入沉思,雖然筆記本渣渣配置,可是運行了6分鐘,差距太大

總結

  • 函數裝飾器就是用來裝飾函數的函數,並使用內置函數替代被裝飾函數
  • 被裝飾函數在定義時就運行,無需顯示調用
  • 內置裝飾器functools.wraps能夠爲被裝飾函數賦值內置屬性
  • 參數化裝飾器能夠接受不一樣參數並適用於被裝飾函數,顯示不一樣的做用,至關於裝飾器中的定製化
  • 參數化的裝飾器嵌套層次較深,邏輯相對複雜
  • 內置裝飾器functools.lru_cache採用緩存機制,存儲須要屢次調用的函數值,從而下降代碼運行時間
相關文章
相關標籤/搜索