Passing parameter to a python class when initializing it asynchronously

Question:

I was looking at this answer where they explain how to initialize a class asynchronously with the method __await__. The question is: is it possible to pass parameters when awaiting the initialization of the class, just like when it is initialized synchronously?

In other words, I’d like to be able to do
my_class = await MyClass(my_parameter), however I wasn’t able to make it work in any way.

Should I just fall back to using the classic __init__ like in this answer?

Asked By: Federico Corazza

||

Answers:

You should just use __init__. You are creating a regular class instance first, and then await on the instance. These are two separate actions.

For example, you could first create the instance, and then later, separately, await on it:

my_class = MyClass(my_parameter)
result_from_coroutine = await my_class

or you could create a task from it and have the event loop execute it with

my_class = MyClass(my_parameter)
task = asyncio.create_task(my_class)  # the loop will await the task
# ...
if task.done():
    result_from_coroutine = task.result()

The __await__ method is what await or the event loop use to drive coroutines. The same separation applies to coroutine functions (defined with async def); they too create a new coroutine object when you call them, and you don’t have to await on them immediately. You can use await on the result at another time.

If you are looking for asynchronous instance creation, then you could hack that up by making the __new__ method into a coroutine:

>>> class Async:
...     async def __new__(cls):
...         instance = super().__new__(cls)
...         return instance
...
>>> Async()
<coroutine object Async.__new__ at 0x103654148>

Awaiting on the coroutine would create the actual instance and return it.

Take into account that this does mean that the __init__ method will be skipped; the latter is only called when the __new__ method directly returns an instance of the class (or a subclass), and a coroutine is not such an instance. You’d have to explicitly do so yourself:

class Async:
    async def __new__(cls, *args, **kwargs):
        instance = super().__new__(cls)
        instance.__init__(*args, **kwargs)
        return instance

at which point you could decide to make the __init__ method a coroutine too.

Note that this is really going against the grain. I’d postpone calling dependent coroutines to a later point instead.

For example, you can just store the parameters to the class on the instance and use those when the instance is awaited on (by having __await__ called), exactly as the post you link to advices you to do.

Answered By: Martijn Pieters

In other words, I’d like to be able to do my_class = await MyClass(my_parameter), however I wasn’t able to make it work in any way.

You can make MyClass awaitable by implementing __await__:

class MyClass:
    def __init__(self, delay):
        self.delay = delay

    async def __await__(self):
        await asyncio.sleep(self.delay)

asyncio.run(MyClass(2)) # sleeps 2 seconds

The code in the linked answer does a similar thing, but it is more complex because it assumes that __init__ itself requires await. If that is not the case, and your __init__ is in fact trivial, but you want the returned instance to be awaitable, you don’t need the added complexity of a split initialization.

Note: despite being recommended, asyncio.run() is still marked provisional. In the above example it can be easily replaced with run_until_complete.

Answered By: user4815162342