Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Creating a temporary async timer callback to a bound method with python-asyncio

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.

like image 791
pumphaus Avatar asked Oct 19 '15 14:10

pumphaus


2 Answers

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)

Important:

  • 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.

like image 109
Huazuo Gao Avatar answered Sep 30 '22 11:09

Huazuo Gao


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())
like image 36
Mikhail Gerasimov Avatar answered Sep 30 '22 09:09

Mikhail Gerasimov