类作为Python中常规函数和协程的装饰器

时间:2019-09-12 18:02:53

标签: python-asyncio python-3.7 python-decorators

我正在尝试创建一个作为装饰器的类,该类将对装饰后的函数应用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__,但又会怎样呢?

1 个答案:

答案 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方法返回的。