3

我试图编写一个装饰器函数,它包装一个asyncio.coroutine并返回完成所花费的时间。下面的配方包含按我预期工作的代码。我唯一的问题是,尽管使用了@functools.wraps. 如何保留原有协程的名称?我检查了来源asyncio.

import asyncio
import functools
import random
import time

MULTIPLIER = 5

def time_resulted(coro):
    @functools.wraps(coro)
    @asyncio.coroutine
    def wrapper(*args, **kargs):
        time_before = time.time()
        result = yield from coro(*args, **kargs)
        if result is not None:
            raise TypeError('time resulted coroutine can '
                'only return None')
        return time_before, time.time()
    print('= wrapper.__name__: {!r} ='.format(wrapper.__name__))
    return wrapper

@time_resulted
@asyncio.coroutine
def random_sleep():
    sleep_time = random.random() * MULTIPLIER
    print('{} -> {}'.format(time.time(), sleep_time))
    yield from asyncio.sleep(sleep_time)

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    tasks = [asyncio.Task(random_sleep()) for i in range(5)]
    loop.run_until_complete(asyncio.wait(tasks))
    loop.close()
    for task in tasks:
        print(task, task.result()[1] - task.result()[0])
    print('= random_sleep.__name__: {!r} ='.format(
        random_sleep.__name__))
    print('= random_sleep().__name__: {!r} ='.format(
        random_sleep().__name__))

结果:

= wrapper.__name__: 'random_sleep' =
1397226479.00875 -> 4.261069174838891
1397226479.00875 -> 0.6596335046471768
1397226479.00875 -> 3.83421163259601
1397226479.00875 -> 2.5514027672929713
1397226479.00875 -> 4.497471439365472
Task(<wrapper>)<result=(1397226479.00875, 1397226483.274884)> 4.266134023666382
Task(<wrapper>)<result=(1397226479.00875, 1397226479.6697)> 0.6609499454498291
Task(<wrapper>)<result=(1397226479.00875, 1397226482.844265)> 3.835515022277832
Task(<wrapper>)<result=(1397226479.00875, 1397226481.562422)> 2.5536720752716064
Task(<wrapper>)<result=(1397226479.00875, 1397226483.51523)> 4.506479978561401
= random_sleep.__name__: 'random_sleep' =
= random_sleep().__name__: 'wrapper' =

如您所见random_sleep(),返回一个具有不同名称的生成器对象。我想保留装饰协程的名称。我不知道这个问题是否是特定的asyncio.coroutines。我还尝试了具有不同装饰器命令的代码,但结果都相同。如果我发表评论@functools.wraps(coro),那么甚至random_sleep.__name__wrapper像我预期的那样。

编辑:我已将此问题发布到 Python 问题跟踪器,并收到了 R. David Murray 的以下回答:“我认为这是一个更普遍的需要改进‘包装’的具体案例,这在 python-dev 上讨论过很久以前。”

4

2 回答 2

2

问题是functools.wraps只改变wrapper.__name__wrapper().__name__保持不变wrapper__name__是一个只读的生成器属性。您可以使用exec设置适当的名称:

import asyncio
import functools
import uuid
from textwrap import dedent

def wrap_coroutine(coro, name_prefix='__' + uuid.uuid4().hex):
    """Like functools.wraps but preserves coroutine names."""
    # attribute __name__ is not writable for a generator, set it dynamically
    namespace = {
        # use name_prefix to avoid an accidental name conflict
        name_prefix + 'coro': coro,
        name_prefix + 'functools': functools,
        name_prefix + 'asyncio': asyncio,
    }
    exec(dedent('''
        def {0}decorator({0}wrapper_coro):
            @{0}functools.wraps({0}coro)
            @{0}asyncio.coroutine
            def {wrapper_name}(*{0}args, **{0}kwargs):
                {0}result = yield from {0}wrapper_coro(*{0}args, **{0}kwargs)
                return {0}result
            return {wrapper_name}
        ''').format(name_prefix, wrapper_name=coro.__name__), namespace)
    return namespace[name_prefix + 'decorator']

用法:

def time_resulted(coro):
    @wrap_coroutine(coro)
    def wrapper(*args, **kargs):
        # ...
    return wrapper

它有效,但可能有比使用exec().

于 2014-05-04T06:08:47.153 回答
0

自从提出这个问题以来,更改协程的名称成为可能。它是通过设置__qualname__(not __name__) 来完成的:

async def my_coro(): pass

c = my_coro()
print(repr(c))
# <coroutine object my_coro at 0x7ff8a7d52bc0>

c.__qualname__ = 'flimflam'
print(repr(c))
# <coroutine object flimflam at 0x7ff8a7d52bc0>

import asyncio
print(repr(asyncio.ensure_future(c)))
# <Task pending name='Task-737' coro=<flimflam() running at <ipython-input>:1>>

__qualname__在协程对象中的用法在 CPython 源代码__repr__中定义

于 2021-05-19T19:00:06.610 回答