I'm trying to create a sort of timer callback to a bound async
method using asyncio
's event loop. The problem now is that the bound async method should not hold a strong reference to the instance, otherwise the latter will never be deleted. The timer callback should only live as long as the parent instance.
I've found a solution, but I don't think it's pretty:
import asyncio
import functools
import weakref
class ClassWithTimer:
def __init__(self):
asyncio.ensure_future(
functools.partial(
ClassWithTimer.update, weakref.ref(self)
)()
)
def __del__(self):
print("deleted ClassWithTimer!")
async def update(self):
while True:
await asyncio.sleep(1)
if self() is None: break
print("IN update of object " + repr(self()))
async def run():
foo = ClassWithTimer()
await asyncio.sleep(5)
del foo
loop = asyncio.get_event_loop()
loop.run_until_complete(run())
Is there a better, more pythonic way to do this? The timer callback really needs to be async.
Without asyncio
, weakref.WeakMethod
would probably be the way to go. But asyncio.ensure_future
requires a coroutine object, so it won't work in this case.
I came across almost the same problem few months ago, and I wrote a decorator to get around it:
def weakmethod(f):
@property
def get(self):
return f.__get__(weakref.proxy(self))
# self = weakref.proxy(self)
# if hasattr(f, '__get__'):
# raise RuntimeWarning(
# 'weakref may not work unless you implement '
# 'the property protocol carefully by youself!'
# )
# return f.__get__(self)
# if asyncio.iscoroutinefunction(f):
# #Make the returned method a coroutine function, optional
# async def g(*arg, **kwarg):
# return await f(self, *arg, **kwarg)
# else:
# def g(*arg, **kwarg):
# return f(self, *arg, **kwarg)
# return g
# #Still some situations not taken into account?
return get
Your code could then be rewritten in much a natural way:
class ClassWithTimer:
def __init__(self):
asyncio.ensure_future(self.update())
def __del__(self):
print("deleted ClassWithTimer!")
@weakmethod
async def update(self):
while True:
await asyncio.sleep(1)
print("IN update of object ", self)
weakref.proxy
does not prevent you from acquiring a strong reference. In addition, it is not guaranteed to behave exactly the same as the original object.
My implementation has not covered all possibilities.
Sorry, I'm not sure if I understood your question correctly. Is this solution you're looking for?
import asyncio
class ClassWithTimer:
async def __aenter__(self):
self.task = asyncio.ensure_future(self.update())
async def __aexit__(self, *args):
try:
self.task.cancel() # Just cancel updating when we don't need it.
await self.task
except asyncio.CancelledError: # Ignore CancelledError rised by cancelled task.
pass
del self # I think you don't need this in real life: instance should be normally deleted by GC.
def __del__(self):
print("deleted ClassWithTimer!")
async def update(self):
while True:
await asyncio.sleep(1)
print("IN update of object " + repr(self))
async def run():
async with ClassWithTimer(): # Use context manager to handle when we need updating.
await asyncio.sleep(5)
loop = asyncio.get_event_loop()
loop.run_until_complete(run())
Output:
IN update of object <__main__.ClassWithTimer object at 0x000000468D0FB208>
IN update of object <__main__.ClassWithTimer object at 0x000000468D0FB208>
IN update of object <__main__.ClassWithTimer object at 0x000000468D0FB208>
IN update of object <__main__.ClassWithTimer object at 0x000000468D0FB208>
deleted ClassWithTimer!
[Finished in 5.2s]
One more way without context manager:
import asyncio
class ClassWithTimer:
def __init__(self):
self.task = asyncio.ensure_future(self.update())
async def release(self):
try:
self.task.cancel()
await self.task
except asyncio.CancelledError:
pass
del self
def __del__(self):
print("deleted ClassWithTimer!")
async def update(self):
while True:
await asyncio.sleep(1)
print("IN update of object " + repr(self))
async def run():
foo = ClassWithTimer()
await asyncio.sleep(5)
await foo.release()
loop = asyncio.get_event_loop()
loop.run_until_complete(run())
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With