这一节介绍一些常见的装饰器实例。
8. 常见的装饰器
8.1 pysnooper
一个有着13k以上star的调试信息打印器。通过装饰器来使用。节1中的示例可以看成对pysnooper的一个模仿。
import pysnooper
@pysnooper.snoop()
def number_to_bits(number):
if number:
bits = []
while number:
number, remainder = divmod(number, 2)
bits.insert(0, remainder)
return bits
else:
return [0]
number_to_bits(6)
# --output--
Source path:... /my_code/foo.py
Starting var:.. number = 6
15:29:11.327032 call 4 def number_to_bits(number):
15:29:11.327032 line 5 if number:
15:29:11.327032 line 6 bits = []
New var:....... bits = []
15:29:11.327032 line 7 while number:
15:29:11.327032 line 8 number, remainder = divmod(number, 2)8.2 静态变量
python缺少其它语言都有的静态变量。静态变量是指只具有函数作用域,但又有全局生命期的那些变量。它们在函数首次进入时初始化,不同于其它栈上的变量的是,即使函数结束退出,它们仍然持有值;再次进入函数时,仍可以访问。静态变量减少了不必要地将变量定义为全局变量(或者类变量)的需要,在一些场合下,可以使得程序代码更为简洁。
由于python的函数就是对象,因此可以为它添加一些属性。考虑到静态变量要求只初始化一次,又只在函数内部使用,因此这是使用装饰器的好场合。
def static_vars(**kwargs):
def decorate(func):
for k in kwargs:
setattr(func, k, kwargs[k])
return func
return decorate
@static_vars(counter = 0)
def echo():
echo.counter += 1
if echo.counter < 3:
print("awesome!")
for i in range(3):
echo()echo将被调用3次,但只打印出两次awesome。因此,这里的静态变量起了作用。
8.3 singleton
# https://realpython.com/primer-on-python-decorators/#stateful-decorators
import functools
def singleton(cls):
"""Make a class a Singleton class (only one instance)"""
@functools.wraps(cls)
def wrapper_singleton(*args, **kwargs):
if not wrapper_singleton.instance:
wrapper_singleton.instance = cls(*args, **kwargs)
return wrapper_singleton.instance
wrapper_singleton.instance = None
return wrapper_singleton
@singleton
class TheOne:
pass
first_one = TheOne()
another_one = TheOne()
id(first_one) == id(another_one) # True8.4 异步化
asyncio被认为是python 3.5以来最重要的更新,会显著提升io性能。但是,并不是所有的库都已经使用asyncio改写了。如果我们的程序已经使用了asyncio,又不得不依赖于某个同步库,这时我们往往会用concurrent库里的相关函数将其异步化。
def async_run(executors):
def decorator(func):
@functools.wraps(func)
async def wrapper(*args, **kwargs):
p = functools.partial(func, *args, **kwargs)
return await asyncio.get_running_loop().run_in_executor(executors, p)
return wrapper
return decorator
from concurrent.futures.thread import ThreadPoolExecutor
executors = ThreadPoolExecutor()
@async_run(executors)
def my_blocking_func():
pass这里的关键是第6行。asyncio.get_running_loop().run_in_executor将在executros中的一个线程中执行,并且立即返回一个future对象。这里还要注意的是,一般地,功能函数(这里是my_blocking_func)和替换函数(这里是wrapper)一般应有相同的签名;这里是一个特例。这种写法会导致类型检查错误地警告提示,但目前并没有好的办法解决,如果有一天IDE的类型检查功能有了更强的推断能力,这个警告可能会消失。
要点: 1. 替换函数(这里是wrapper)必须声明为async,否则无法跟更外层的async函数连接起来。但outer decorator和inner decorator都不应该声明为异步的,因为它们在加载时就被执行求值了。如果深入了解了装饰器的执行顺序,这一点会很容易理解。 2. 功能函数(例子中是my_blocking_func)必须声明为普通函数,这是因为它们由run_in_executor来调用,而run_in_executor只会将其当普通函数对待。
再给出在unittest中使用异步化的例子。到目前为止,unittest是不支持异步执行的,我们可以通过装饰器来增加这一能力。
import asyncio
import functools
def async_test(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
loop = asyncio.get_event_loop()
return loop.run_until_complete(func(*args, **kwargs))
return wrapper
# 然后将装饰器运用在测试用例上:
@async_test
async def setUp(self) -> None:
pass
@async_test
async def test_000_something(self):
pass8.5 其它
比如用户检查,函数执行时间审计等。这些都是常见的AOP的例子,此处不再赘述。
9. 结论
在适合面向切面编程的场合都可使用装饰器来使得代码更简洁易读。当模块加载器读到'@'装饰器语句时,会执行 func = decorator(func)来替换功能函数。
假设要编写一个名为snoop的装饰器,则其签名应该定义为def snoop(func);其函数体应根据装饰器的实际作用来定义,但必须返回一个替换函数,对功能函数的调用一般应该在这个替换函数中调用。替换函数一般应该具有与功能函数一样的签名;如果功能函数有返回值,则替换函数也应该将调用结果返回。在注解中使用这种装饰函数时,不能带括号引用。
替换函数完成对功能函数的替换后,会导致堆栈信息错误,因此我们需要在替换函数之上,添加一个@functools.wraps(func)的注解,以将功能函数的相关元信息复制到替换函数对象上。上述调用一定是有参调用。
如果装饰函数自身有参数,则需要在上述函数之上,再封装一层(我们称之为outer decorator,之前的装饰器称之为inner dcorator)。我们在注解中使用装饰函数时,需要带括号调用装饰函数,并传入参数。因此装饰函数的签名应该反映这些参数的存在。
思考题
- 装饰器函数可以声明为async吗?
- 在使用装饰器来注册信号处理函数里,如果在装饰器里对原功能函数进行了封装。考虑这样两种情况,先对原函数进行封装,再用封装后的函数与信号绑定;或者先将信号与原函数绑定,再对原函数进行封装,并返回封装函数。这两者区别何在?两种情况都存在哪些副作用?
- 在使用装饰器来注册信号处理函数是会出现缺少self对象的情况,而有些情况下又会多出来self对象,为何会出现这样的情况?
- 用装饰器来注册信号处理函数是会出现缺少self对象的情况,而有些情况下又会多出来self对象,为何会出现这样的情况?
相关文章
杨勇:Python装饰器完全指南(1)zhuanlan.zhihu.com
杨勇:Python装饰器完全指南(2)zhuanlan.zhihu.com