Using async await in Python __init__ method
Asked Answered
C

2

9

I am writing a class and want to use an async function in the __init__ method to set up some variables needed for the class. The problem is, I can't do that, because __init__ has to be synchronous.

Here is the relevant part of my code (edited for simplicity, the logic remains the same):

# This has to be called outside of the class
asyncDatabaseConnection = startDBConnection()

class discordBot(discord.Client):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        # Init is only run once, but we cant use async stuff here
        self.firstRun = True

    async def on_ready(self):
        # Other stuff happens here but it doesen't involve this question

        # on_ready is called when bot is ready, but can be called multiple times when running
        # (if bot has to reconnect to API), so we have to check
        if self.firstRun:
            await asyncDatabaseConnection.setValue("key", "value")
            self.firstRun = False

if __name__ == "__main__":
    # Instance class and start async stuff
    bot = discordBot()
    bot.run()

As you can see, it is for a Discord bot, but that doesn't really matter, it's more about the logic.

The function I want to call is asyncDatabaseConnection.setValue("key", "value").

Like I said, I cannot call it from __init__ because __init__ has to be synchronous, so instead I set firstRun to True during the init call, and then I can use that later on to tell if the code has been run before or not

on_ready is a function that is called when the bot is ready to start sending/receiving data, so I can use it as sort of a second __init__. The problem comes from the fact that on_ready can be called multiple times throughout the running of the program, meaning I have to have the firstRun check I described before.

This seems like a lot of code just to do 1 thing at startup (as well as adding overhead, however small, when on_ready is called). Is there a cleaner way of doing this?

Cholecystitis answered 15/4, 2019 at 19:28 Comment(2)
I would argue that you should have a function that does the async task and then synchronously initializes and returns an instance of the class.Smitty
duplicate of How to set class attribute with await in __init__. see also Python: Defining a class with an async constructor (3 ways)Sporogony
E
14

It's a little awkward, but you can create a Task, then run it and get its result. If you do this often, it may help to write a helper function:

def run_and_get(coro):
    task = asyncio.create_task(coro)
    asyncio.get_running_loop().run_until_complete(task)
    return task.result()

class discordBot(discord.Client):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        run_and_get(asyncDatabaseConnection.setValue("key", "value"))

This depends on there being a running event loop, which I believe Client.__init__ sets up

Economically answered 15/4, 2019 at 20:13 Comment(7)
This seems like it might solve my problem. Why do you turn coro into a Task? Is it just to be able to get a return value? If I don't need a return value, can I just do run_until_complete(coro)?Cholecystitis
I think if you do that it just creates a Task or Future for that coroutine under the hood, so there shouldn't be any performance implications.Economically
I get This event loop is already runningThionic
@Thionic please create a minimal reproducible example and ask your own question.Rucksack
> Comments are used to ask for clarification or to point out problems in the post.Thionic
I have the same problem as @Hacker. The problem is that this code only works in code that's not called via async code, e.g. if async def outer() calls def inner(), and inner() tries calling run_and_get(), that's when the problem arises.Diarmid
@Diarmid That's because the event loop is already blocking waiting on outer, and there's no way from inside inner to communicate that it should pause outer and execute whatever coroutine run_and_get wants to run. The short answer is to refactor. You need to segregate your code so that it's either all async or so that there's only an async shell that calls regular functions. I can't really give specific advice here without knowing exactly how your program is designed. Maybe inner should be returning tasks and then the caller can decide whether to run_and_get or await?Economically
L
0

Unfortunately there's no straightforward way to do this. More generally, you can only do two of the following three:

  1. block on async code...
  2. ...called inside a non-async call...
  3. ...when an async loop is already active in the current thread.

In other words, you can either (a) block on async code called inside a non-async call, so long there is no running loop in the current thread; or (b) make a non-blocking async call inside the non-async callable.

In your particular case, since your options are constrained by the Discord framework, I'd either consider moving the async call to the on_connect event, or if that doesn't work as well, using a bit of runtime polymorphism:

class discordBot(discord.Client):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.on_ready = self.__first_on_ready

    async def __first_on_ready(self):
        # Run the stuff that normally happens in on_ready
        await self.__on_ready()

        # Initialize the database.
        await asyncDatabaseConnection.setValue("key", "value")

        # Update the event handler.
        self.on_ready = self.__on_ready

    async def __on_ready(self):
        # Routine on_ready stuff.

That way the database code runs only on the first on_ready call, and is skipped in subsequent events.

Lingua answered 15/7, 2024 at 21:19 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.