Wie dekoriere ich eine asyncio.coroutine, um ihren __name__ beizubehalten?
-
21-12-2019 - |
Frage
Ich habe versucht, eine Dekoratorfunktion zu schreiben, die eine umschließt asyncio.coroutine
und gibt die Zeit zurück, die für die Erledigung benötigt wurde.Das Rezept unten enthält den Code, der wie erwartet funktioniert.Mein einziges Problem dabei ist, dass ich trotz der Verwendung von irgendwie den Namen der dekorierten Funktion verliere @functools.wraps
.Wie kann der Name der ursprünglichen Coroutine beibehalten werden?Ich habe die Quelle überprüft 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__))
Das Ergebnis:
= 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' =
Wie du sehen kannst random_sleep()
gibt ein Generatorobjekt mit einem anderen Namen zurück.Ich möchte den Namen der dekorierten Coroutine beibehalten.Mir ist nicht bekannt, ob dieses Problem spezifisch ist asyncio.coroutines
oder nicht.Ich habe den Code auch mit verschiedenen Dekorationsaufträgen ausprobiert, aber alle haben das gleiche Ergebnis erzielt.Wenn ich kommentiere @functools.wraps(coro)
dann sogar random_sleep.__name__
wird wrapper
wie ich erwartet habe.
BEARBEITEN:Ich habe dieses Problem im Python Issue Tracker gepostet und die folgende Antwort von R erhalten.David Murray:„Ich denke, dies ist ein konkreter Fall einer allgemeineren Notwendigkeit, ‚Wraps‘ zu verbessern, die vor nicht allzu langer Zeit auf python-dev diskutiert wurde.“
Lösung
Das Problem ist das functools.wraps
nur Änderungen wrapper.__name__
Und wrapper().__name__
bleibt wrapper
. __name__
ist ein schreibgeschütztes Generatorattribut.Du könntest benutzen exec
So legen Sie den passenden Namen fest:
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']
Verwendung:
def time_resulted(coro):
@wrap_coroutine(coro)
def wrapper(*args, **kargs):
# ...
return wrapper
Es funktioniert, aber es gibt wahrscheinlich einen besseren Weg als die Verwendung exec()
.