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()中的语句。

被装饰函数运行时,其本身的内容(示例中print语句)并没有被执行,而是运行了装饰器函数中的print语句;这就是装饰器的作用,替代被装饰函数,同时装饰器也可以调用外界自由变量(registry),从而引出一个重要概念:

闭包

实例中registry变量和register函数组合的共同体,被成称为闭包。

该例中有两个不太寻常的地方:

  • 装饰器函数和被装饰函数定义在一个模块之中;一般来说,两者应该定义在不同的模块;
  • 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()

运行结果:

**************************************** 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采用缓存机制,存储需要多次调用的函数值,从而降低代码运行时间
    原文作者:逸远尘红
    原文地址: https://segmentfault.com/a/1190000016048526
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞