Python函数装饰器高级用法

开发 后端
在了解了Python函数装饰器基础知识和闭包之后,开始正式学习函数装饰器。这是装饰器的典型行为:把被装饰的函数换成新函数,二者接受相同的参数,而且返回被装饰的函数本该返回的值,同时还会做些额外操作。

[[402859]]

本文转载自微信公众号「dongfanger」,作者dongfanger。转载本文请联系dongfanger公众号。

在了解了Python函数装饰器基础知识和闭包之后,开始正式学习函数装饰器。

典型的函数装饰器

以下示例定义了一个装饰器,输出函数的运行时间:

函数装饰器和闭包紧密结合,入参func代表被装饰函数,通过自由变量绑定后,调用函数并返回结果。

使用clock装饰器:

  1. import time 
  2. from clockdeco import clock 
  3.  
  4. @clock 
  5. def snooze(seconds): 
  6.     time.sleep(seconds) 
  7.  
  8. @clock 
  9. def factorial(n): 
  10.     return 1 if n < 2 else n*factorial(n-1) 
  11.  
  12. if __name__=='__main__'
  13.     print('*' * 40, 'Calling snooze(.123)'
  14.     snooze(.123) 
  15.     print('*' * 40, 'Calling factorial(6)'
  16.     print('6! =', factorial(6))  # 6!指6的阶乘 

输出结果:

这是装饰器的典型行为:把被装饰的函数换成新函数,二者接受相同的参数,而且返回被装饰的函数本该返回的值,同时还会做些额外操作。

值得注意的是factorial()是个递归函数,从结果来看,每次递归都用到了装饰器,打印了运行时间,这是因为如下代码:

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

等价于:

  1. def factorial(n): 
  2.     return 1 if n < 2 else n*factorial(n-1) 
  3.      
  4. factorial = clock(factorial) 

factorial引用的是clock(factorial)函数的返回值,也就是装饰器内部函数clocked,每次调用factorial(n),执行的都是clocked(n)。

叠放装饰器

  1. @d1 
  2. @d2 
  3. def f(): 
  4.     print("f"

等价于:

  1. def f(): 
  2.     print("f"
  3.  
  4. f = d1(d2(f)) 

参数化装饰器

怎么让装饰器接受参数呢?答案是:创建一个装饰器工厂函数,把参数传给它,返回一个装饰器,然后再把它应用到要装饰的函数上。

示例如下:

  1. registry = set() 
  2.  
  3. def register(active=True): 
  4.     def decorate(func): 
  5.         print('running register(active=%s)->decorate(%s)' 
  6.               % (active, func)) 
  7.         if active: 
  8.             registry.add(func) 
  9.         else
  10.             registry.discard(func) 
  11.  
  12.         return func 
  13.     return decorate 
  14.  
  15. @register(active=False
  16. def f1(): 
  17.     print('running f1()'
  18.  
  19. # 注意这里的调用 
  20. @register() 
  21. def f2(): 
  22.     print('running f2()'
  23.  
  24. def f3(): 
  25.     print('running f3()'

register是一个装饰器工厂函数,接受可选参数active默认为True,内部定义了一个装饰器decorate并返回。需要注意的是装饰器工厂函数,即使不传参数,也要加上小括号调用,比如@register()。

再看一个示例:

  1. import time 
  2.  
  3. DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}' 
  4.  
  5. # 装饰器工厂函数 
  6. def clock(fmt=DEFAULT_FMT): 
  7.     # 真正的装饰器 
  8.     def decorate(func):  
  9.         # 包装被装饰的函数 
  10.         def clocked(*_args): 
  11.             t0 = time.time() 
  12.             # _result是被装饰函数返回的真正结果 
  13.             _result = func(*_args)   
  14.             elapsed = time.time() - t0 
  15.             name = func.__name__ 
  16.             args = ', '.join(repr(arg) for arg in _args)  
  17.             result = repr(_result)  
  18.             # **locals()返回clocked的局部变量 
  19.             print(fmt.format(**locals()))   
  20.             return _result  
  21.         return clocked   
  22.     return decorate  
  23.  
  24. if __name__ == '__main__'
  25.  
  26.     @clock()   
  27.     def snooze(seconds): 
  28.         time.sleep(seconds) 
  29.  
  30.     for i in range(3): 
  31.         snooze(.123) 

这是给典型的函数装饰器添加了参数fmt,装饰器工厂函数增加了一层嵌套,示例中一共有3个def。

标准库中的装饰器

Python内置了三个用于装饰方法的函数:property、classmethod和staticmethod,这会在将来的文章中讲到。本文介绍functools中的三个装饰器:functools.wraps、functools.lru_cache和functools.singledispatch。

functools.wraps

Python函数装饰器在实现的时候,被装饰后的函数其实已经是另外一个函数了(函数名等函数属性会发生改变),为了不影响,Python的functools包中提供了一个叫wraps的装饰器来消除这样的副作用(它能保留原有函数的名称和函数属性)。

示例,不加wraps:

  1. def my_decorator(func): 
  2.     def wrapper(*args, **kwargs): 
  3.         '''decorator''' 
  4.         print('Calling decorated function...'
  5.         return func(*args, **kwargs) 
  6.     return wrapper 
  7.  
  8. @my_decorator 
  9. def example(): 
  10.     """Docstring""" 
  11.     print('Called example function'
  12.  
  13. print(example.__name__, example.__doc__) 
  14. # 输出wrapper decorator 

加wraps:

  1. import functools 
  2.  
  3.  
  4. def my_decorator(func): 
  5.     @functools.wraps(func) 
  6.     def wrapper(*args, **kwargs): 
  7.         '''decorator''' 
  8.         print('Calling decorated function...'
  9.         return func(*args, **kwargs) 
  10.     return wrapper 
  11.  
  12. @my_decorator 
  13. def example(): 
  14.     """Docstring""" 
  15.     print('Called example function'
  16.  
  17. print(example.__name__, example.__doc__) 
  18. # 输出example Docstring 

functools.lru_cache

lru是Least Recently Used的缩写,它是一项优化技术,把耗时的函数的结果保存起来,避免传入相同的参数时重复计算。

示例:

  1. import functools 
  2.  
  3. from clockdeco import clock 
  4.  
  5. @functools.lru_cache() 
  6. @clock 
  7. def fibonacci(n): 
  8.     if n < 2: 
  9.         return n 
  10.     return fibonacci(n-2) + fibonacci(n-1) 
  11.  
  12. if __name__=='__main__'
  13.     print(fibonacci(6)) 

优化了递归算法,执行时间会减半。

注意,lru_cache可以使用两个可选的参数来配置,它的签名如下:

  1. functools.lru_cache(maxsize=128, typed=False

maxsize:最大存储数量,缓存满了以后,旧的结果会被扔掉。

typed:如果设为True,那么会把不同参数类型得到的结果分开保存,即把通常认为相等的浮点数和整型参数(如1和1.0)区分开。

functools.singledispatch

Python3.4的新增语法,可以用来优化函数中的大量if/elif/elif。使用@singledispatch装饰的普通函数会变成泛函数:根据第一个参数的类型,以不同方式执行相同操作的一组函数。所以它叫做single dispatch,单分派。

根据多个参数进行分派,就是多分派了。

示例,生成HTML,显示不同类型的Python对象:

  1. import html 
  2.  
  3.  
  4. def htmlize(obj): 
  5.     content = html.escape(repr(obj)) 
  6.     return '<pre>{}</pre>'.format(content) 

 

因为Python不支持重载方法或函数,所以就不能使用不同的签名定义htmlize的变体,只能把htmlize变成一个分派函数,使用if/elif/elif,调用专门的函数,比如htmlize_str、htmlize_int等。时间一长htmlize会变得很大,跟各个专门函数之间的耦合也很紧密,不便于模块扩展。

@singledispatch经过深思熟虑后加入到了标准库,来解决这类问题:

  1. from functools import singledispatch 
  2. from collections import abc 
  3. import numbers 
  4. import html 
  5.  
  6. @singledispatch 
  7. def htmlize(obj): 
  8.     # 基函数 这里不用写if/elif/elif来分派了 
  9.     content = html.escape(repr(obj)) 
  10.     return '<pre>{}</pre>'.format(content) 
  11.  
  12. @htmlize.register(str) 
  13. def _(text): 
  14.     # 专门函数 
  15.     content = html.escape(text).replace('\n''<br>\n'
  16.     return '<p>{0}</p>'.format(content) 
  17.  
  18. @htmlize.register(numbers.Integral)  
  19. def _(n): 
  20.     # 专门函数 
  21.     return '<pre>{0} (0x{0:x})</pre>'.format(n) 
  22.  
  23. @htmlize.register(tuple) 
  24. @htmlize.register(abc.MutableSequence) 
  25. def _(seq): 
  26.     # 专门函数 
  27.     inner = '</li>\n<li>'.join(htmlize(item) for item in seq) 
  28.     return '<ul>\n<li>' + inner + '</li>\n</ul>' 

@singledispatch装饰了基函数。专门函数使用@<<base_function>>.register(<<type>>)装饰,它的名字不重要,命名为_,简单明了。

这样编写代码后,Python会根据第一个参数的类型,调用相应的专门函数。

小结

本文首先介绍了典型的函数装饰器:把被装饰的函数换成新函数,二者接受相同的参数,而且返回被装饰的函数本该返回的值,同时还会做些额外操作。接着介绍了装饰器的两个高级用法:叠放装饰器和参数化装饰器,它们都会增加函数的嵌套层级。最后介绍了3个标准库中的装饰器:保留原有函数属性的functools.wraps、缓存耗时的函数结果的functools.lru_cache和优化if/elif/elif代码的functools.singledispatch。

参考资料:

《流畅的Python》

https://github.com/fluentpython/example-code/tree/master/07-closure-deco

https://blog.csdn.net/liuzonghao88/article/details/103586634

 

责任编辑:武晓燕 来源: dongfanger
相关推荐

2010-02-01 17:50:32

Python装饰器

2023-02-07 07:47:52

Python装饰器函数

2022-04-04 09:12:18

Python内置函数

2022-10-21 07:50:35

装饰器Python编程

2024-11-04 15:30:43

Python装饰器函数

2024-09-23 09:00:00

装饰器函数代码

2022-09-19 23:04:08

Python装饰器语言

2016-11-01 09:24:38

Python装饰器

2021-05-27 07:12:19

Python函数装饰器

2024-05-24 11:36:28

Python装饰器

2023-12-11 15:51:00

Python装饰器代码

2024-02-22 10:14:40

Filter函数Python

2022-09-21 09:04:07

Python装饰器

2021-04-11 08:21:20

Python@property装饰器

2023-12-13 13:28:16

装饰器模式Python设计模式

2024-09-12 15:32:35

装饰器Python

2022-09-27 11:01:08

Python装饰器

2024-02-26 00:00:00

TypeScript装饰器decorators

2021-07-27 15:58:12

Python日志代码

2017-03-10 10:16:37

PythonRequests库
点赞
收藏

51CTO技术栈公众号