2

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?

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Facorazza
  • 317
  • 1
  • 15

2 Answers2

2

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.

yEmreAk.com
  • 3,278
  • 2
  • 18
  • 37
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • The reason why I'm doing an asynchronous initialization is because I'm awaiting a few methods that need to be run as soon as the class is created, but because they are private methods I didn't want to call them "outside" of the class itself. – Facorazza Sep 16 '18 at 22:44
  • @FedericoCorazza: you can't create instances asynchronously. – Martijn Pieters Sep 16 '18 at 22:46
  • @FedericoCorazza: that's because the process of creating instances isn't handled by `__init__`, that's just a hook called *after* the instance is already created. – Martijn Pieters Sep 16 '18 at 22:47
  • This is very interesting, but I agree, I should call the awaitables later on. – Facorazza Sep 16 '18 at 22:56
0

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.

user4815162342
  • 141,790
  • 18
  • 296
  • 355
  • I'll play with this a little bit. Thank you – Facorazza Sep 18 '18 at 07:04
  • @FedericoCorazza Sorry about the rejected edit, I didn't know that it was done by the same person who asked the question. I've now amended the answer to mention the provisional status of `asyncio.run`. – user4815162342 Sep 18 '18 at 07:38