我正在尝试创建一个作为装饰器的类,该类将对装饰后的函数应用try-except块并保留一些异常日志。我想将装饰器应用于常规函数和协程。
我已经完成了类装饰器的工作,并且它按常规功能设计,但是协程程序出了点问题。以下是一些简化的代码,用于简化类装饰器和几个用例:
import traceback
import asyncio
import functools
class Try:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
def __call__(self, *args, **kwargs):
print(f"applying __call__ to {self.func.__name__}")
try:
return self.func(*args, **kwargs)
except:
print(f"{self.func.__name__} failed")
print(traceback.format_exc())
def __await__(self, *args, **kwargs):
print(f"applying __await__ to {self.func.__name__}")
try:
yield self.func(*args, **kwargs)
except:
print(f"{self.func.__name__} failed")
print(traceback.format_exc())
# Case 1
@Try
def times2(x):
return x*2/0
# Case 2
@Try
async def times3(x):
await asyncio.sleep(0.0001)
return x*3/0
async def test_try():
return await times3(10)
def main():
times2(10)
asyncio.run(test_try())
print("All done")
if __name__ == "__main__":
main()
以下是上面代码的输出(稍作修改):
applying __call__ to times2
times2 failed
Traceback (most recent call last):
File "<ipython-input-3-37071526b2e6>", line 14, in __call__
return self.func(*args, **kwargs)
File "<ipython-input-3-37071526b2e6>", line 30, in times2
return x*2/0
ZeroDivisionError: division by zero
applying __call__ to times3
Traceback (most recent call last):
File "[...]/lib/python3.7/site-packages/IPython/core/interactiveshell.py", line 3296, in run_code
exec(code_obj, self.user_global_ns, self.user_ns)
File "<ipython-input-3-37071526b2e6>", line 46, in <module>
main()
File "<ipython-input-3-37071526b2e6>", line 43, in main
asyncio.run(test_try())
File "[...]/lib/python3.7/asyncio/runners.py", line 43, in run
return loop.run_until_complete(main)
File "[...]/lib/python3.7/asyncio/base_events.py", line 579, in run_until_complete
return future.result()
File "<ipython-input-3-37071526b2e6>", line 39, in test_try
return await times3(10)
File "<ipython-input-3-37071526b2e6>", line 36, in times3
return x*3/0
ZeroDivisionError: division by zero
案例1的行为正常:按预期方式调用__call__
,然后修饰的函数失败并捕获异常。但是我无法解释案例2的行为。请注意,最后缺少“ times3失败”和“全部完成”打印。我无法在此处重现颜色编码的输出,但案例1的回溯是常规打印,而案例2的回溯是异常红色(在PyCharm上)。令人惊讶的是,调用了__call__
方法而不是__await__
。
我尝试了另一种作为装饰器的类,它使函数被调用的次数保持一致。对于具有常规功能或协程的__call__
来说,这很好用。
那到底是怎么回事?我是否需要以某种方式强制该函数使用__await__
?怎么样?
我尝试了以下操作:
async def test_try2():
func = await times3
有输出
applying __await__ to times3
times3 failed
Traceback (most recent call last):
File "<ipython-input-5-5a85f988097e>", line 22, in __await__
yield self.func(*args, **kwargs)
TypeError: times3() missing 1 required positional argument: 'x'
哪个会强制使用__await__
,但又会怎样呢?
答案 0 :(得分:0)
您的代码存在的问题是将__await__
放在错误的对象上。通常,await f(x)
扩展为以下内容:
_awaitable = f(x)
_iter = _awaitable.__await__()
yield from _iter # not literally[1]
请注意如何在函数的结果而不是函数对象本身上调用__await__()
。您的times3
示例中将发生以下情况:
__call__
调用times3
中原始的self.func
协程函数,该函数简单地构造了一个协程对象。此时没有异常,因为该对象尚未开始执行,因此返回了coroutine object(通过调用async def
协程函数得到的结果)。
__await__
(原始self.func
times3
)获得的协程对象上调用 async def
,而不是在函数包装上。这是因为,根据上述伪代码,您的包装器对应于f
,而__await__()
在_awaitable
上被调用,在您的情况下,这是调用{{1}的结果}。
通常,您不知道是否会等待函数调用的结果。但是,由于协程对象除了等待对象外没有其他用途(甚至在销毁对象时会发出警告,而无需等待),因此可以放心地假设。此假设使您的f
可以检查函数调用的结果是否可以等待,如果可以,则将其包装在一个对象中,该对象将在__call__
级别上实现包装逻辑:
__await__
这将产生预期的输出:
...
import collections.abc
class Try:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
def __call__(self, *args, **kwargs):
print(f"applying __call__ to {self.func.__name__}")
try:
result = self.func(*args, **kwargs)
except:
print(f"{self.func.__name__} failed")
print(traceback.format_exc())
return
if isinstance(result, collections.abc.Awaitable):
# The result is awaitable, wrap it in an object
# whose __await__ will call result.__await__()
# and catch the exceptions.
return TryAwaitable(result)
return result
class TryAwaitable:
def __init__(self, awaitable):
self.awaitable = awaitable
def __await__(self, *args, **kwargs):
print(f"applying __await__ to {self.awaitable.__name__}")
try:
return yield from self.awaitable.__await__()
except:
print(f"{self.awaitable.__name__} failed")
print(traceback.format_exc())
请注意,您对applying __call__ to times3
applying __await__ to times3
times3 failed
Traceback (most recent call last):
File "wrap3.py", line 30, in __await__
yield from self.awaitable.__await__()
File "wrap3.py", line 44, in times3
return x*3/0
ZeroDivisionError: division by zero
的实现存在一个不相关的问题,它使用__await__
委托给了该函数。必须使用yield
代替,因为这允许底层的可迭代对象选择何时挂起,并在停止挂起时提供值。裸露的yield from
无条件(且仅悬挂一次)悬挂,这与yield
的语义不兼容。
1
字面上不是,因为await
中不允许使用yield from
。但是async def
的行为就像这样的生成器是由它返回的对象的async def
方法返回的。