2

I'd like to define what essentially is an asynchronous __del__ that closes a resource. Here's an example.

import asyncio

class Async:
    async def close(self):
        print('closing')
        return self

    def __del__(self):
        print('destructing')
        asyncio.ensure_future(self.close())

async def amain():
    Async()

if __name__ == '__main__':
    asyncio.run(amain())

This works, printing destructing and closing as expected. However, if the resource is defined outside an asynchronous function, __del__ is called, but closing is never performed.

def main():
    Async()

No warning is raised here, but the prints reveal that closing was not done. The warning is issued if an asynchronous function has been run, but any instance is created outside of it.

def main2():
    Async()
    asyncio.run(amain())

RuntimeWarning: coroutine 'Async.close' was never awaited

This has been the subject in 1 and 2, but neither quite had what I was looking for, or maybe I didn't know how to look. Particularly the first question was about deleting a resource, and its answer suggested using asyncio.ensure_future, which was tested above. Python documentation suggests using the newer asyncio.create_task, but it straight up raises an error in the non-async case, there being no current loop. My final, desperate attempt was to use asyncio.run, which worked for the non-async case, but not for the asynchronous one, as calling run is prohibited in a thread that already has a running loop. Additionally, the documentation states that it should only be called once in a program.

I'm still new to async things. How could this be achieved?


A word on the use case, since asynchronous context managers were mentioned as the preferred alternative in comments. I agree, using them for short-term resource management would be ideal. However, my use case is different for two reasons.

  • Users of the class are not necessarily aware of the underlying resources. It is better user experience to hide closing the resource from a user who doesn't fiddle with the resource itself.
  • The class needs to be instantiated (or for it to be possible to instantiate it) in a synchronous context, and it is often created just once. For example, in a web server context the class would be instantiated in the global scope, after which its async functions would be used in the endpoint definitions.

For example:

asc = Async()

server.route('/', 'GET')
async def root():
    return await asc.do_something(), 200

I'm open to other suggestions of implementing such a feature, but at this point even my curiosity for the possibility that this can be done is enough for me to want an answer to this specific question, not just the general problem.

Felix
  • 2,548
  • 19
  • 48
  • 1
    `__del__` is inappropriate for resource management. You want to use a context manager to ensure some object is closed when you are finished with it. – chepner Aug 20 '20 at 18:08
  • @chepner Thank you for your concern. But e.g. async servers would be rather inconvenient in that case, if the class would be defined outside the endpoints (ie. in global scope) and used in all of them. – Felix Aug 20 '20 at 18:15
  • 1
    I don't think what you want can be achieved, especially if you want to support all the use cases. Triggering async deallocation from sync code is especially an issue, where not only is the event loop not running, but the event loop that will eventually run will be one freshly created by `asyncio.run`. I don't see how to handle that except through gross hacks like monkey-patching `asyncio.run`. chepner was correct in pointing out that async context managers are the way to go here. I'm not sure I understood your reasoning against them; can you elaborate on that? – user4815162342 Aug 20 '20 at 21:09
  • @user4815162342 I detailed my use case in the question! – Felix Aug 20 '20 at 21:36

1 Answers1

1

Only thing that comes to mind is to run cleanup after the server shutdown. It'll look something like this:

asc = Async()

try:
  asyncio.run(run_server())  # You already do it now somewhere
finally:
  asyncio.run(asc.close())

Since asyncio.run creates new event loop each time, you may want to go even deeper and reuse the same event loop:

loop = asyncio.get_event_loop()
asc = Async()

try:
  loop.run_until_complete(run_server())
finally:
  loop.run_until_complete(asc.close())

It's absolutely ok to call run_until_complete multiple times as long as you know what you're doing.


Full example with your snippet:

import asyncio


class Async:
    async def close(self):
        print('closing')
        return self

    async def cleanup(self):
        print('destructing')
        await self.close()


loop = asyncio.get_event_loop()
asc = Async()


async def amain():
    await asyncio.sleep(1)  # Do something


if __name__ == '__main__':
    try:
        loop.run_until_complete(amain())
    finally:
        loop.run_until_complete(asc.cleanup())
        loop.close()
Mikhail Gerasimov
  • 36,989
  • 16
  • 116
  • 159
  • That's fair, even though this is not what I asked for. This is probably the best way to go. Many thanks! – Felix Aug 23 '20 at 17:41