Comment définir l'attribut de classe avec await dans __init__

92

Comment puis-je définir une classe avec awaitdans le constructeur ou le corps de la classe?

Par exemple ce que je veux:

import asyncio

# some code


class Foo(object):

    async def __init__(self, settings):
        self.settings = settings
        self.pool = await create_pool(dsn)

foo = Foo(settings)
# it raises:
# TypeError: __init__() should return None, not 'coroutine'

ou exemple avec l'attribut de corps de classe:

class Foo(object):

    self.pool = await create_pool(dsn)  # Sure it raises syntax Error

    def __init__(self, settings):
        self.settings = settings

foo = Foo(settings)

Ma solution (mais j'aimerais voir une manière plus élégante)

class Foo(object):

    def __init__(self, settings):
        self.settings = settings

    async def init(self):
        self.pool = await create_pool(dsn)

foo = Foo(settings)
await foo.init()
uralbash
la source
1
Vous pourriez avoir de la chance avec __new__, même si ce n'est peut-être pas élégant
JBernardo
Je n'ai pas d'expérience avec la version 3.5, et dans d'autres langues, cela ne fonctionnerait pas à cause de la nature virale de async / await, mais avez-vous essayé de définir une fonction async comme _pool_init(dsn)et de l'appeler ensuite __init__? Cela préserverait l'apparence d'init-in-constructeur.
ap
1
Si vous utilisez curio: curio.readthedocs.io/en/latest/…
matsjoyce
1
utilisez @classmethod😎 c'est un constructeur alternatif. y mettre le travail asynchrone; puis dans __init__, il suffit de définir les selfattributs
grisaitis

Réponses:

117

La plupart des méthodes magiques ne sont pas conçus pour fonctionner avec async def/ await- en général, vous ne devriez utiliser à l' awaitintérieur des méthodes magiques asynchrones dédiés - __aiter__, __anext__, __aenter__et __aexit__. Son utilisation dans d'autres méthodes magiques ne fonctionnera pas du tout, comme c'est le cas avec __init__(à moins que vous n'utilisiez certaines astuces décrites dans d'autres réponses ici), ou vous obligera à toujours utiliser tout ce qui déclenche l'appel de la méthode magique dans un contexte asynchrone.

Les asynciobibliothèques existantes ont tendance à gérer cela de deux manières: Premièrement, j'ai vu le modèle d'usine utilisé ( asyncio-redispar exemple):

import asyncio

dsn = "..."

class Foo(object):
    @classmethod
    async def create(cls, settings):
        self = Foo()
        self.settings = settings
        self.pool = await create_pool(dsn)
        return self

async def main(settings):
    settings = "..."
    foo = await Foo.create(settings)

D'autres bibliothèques utilisent une fonction coroutine de niveau supérieur qui crée l'objet, plutôt qu'une méthode d'usine:

import asyncio

dsn = "..."

async def create_foo(settings):
    foo = Foo(settings)
    await foo._init()
    return foo

class Foo(object):
    def __init__(self, settings):
        self.settings = settings

    async def _init(self):
        self.pool = await create_pool(dsn)

async def main():
    settings = "..."
    foo = await create_foo(settings)

La create_poolfonction à partir de aiopglaquelle vous souhaitez appeler __init__utilise en fait ce modèle exact.

Cela résout au moins le __init__problème. Je n'ai pas vu de variables de classe qui font des appels asynchrones dans la nature dont je puisse me souvenir, donc je ne sais pas si des modèles bien établis ont émergé.

Dano
la source
35

Une autre façon de faire cela, pour le plaisir:

class aobject(object):
    """Inheriting this class allows you to define an async __init__.

    So you can create objects by doing something like `await MyClass(params)`
    """
    async def __new__(cls, *a, **kw):
        instance = super().__new__(cls)
        await instance.__init__(*a, **kw)
        return instance

    async def __init__(self):
        pass

#With non async super classes

class A:
    def __init__(self):
        self.a = 1

class B(A):
    def __init__(self):
        self.b = 2
        super().__init__()

class C(B, aobject):
    async def __init__(self):
        super().__init__()
        self.c=3

#With async super classes

class D(aobject):
    async def __init__(self, a):
        self.a = a

class E(D):
    async def __init__(self):
        self.b = 2
        await super().__init__(1)

# Overriding __new__

class F(aobject):
    async def __new__(cls):
        print(cls)
        return await super().__new__(cls)

    async def __init__(self):
        await asyncio.sleep(1)
        self.f = 6

async def main():
    e = await E()
    print(e.b) # 2
    print(e.a) # 1

    c = await C()
    print(c.a) # 1
    print(c.b) # 2
    print(c.c) # 3

    f = await F() # Prints F class
    print(f.f) # 6

import asyncio
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
khazhyk
la source
2
C'est actuellement la mise en œuvre la plus claire et la plus compréhensible à mon avis. J'aime vraiment à quel point il est intuitivement extensible. J'avais peur qu'il soit nécessaire de fouiller dans les métaclasses.
Tankobot
1
Cela n'a pas de __init__sémantique correcte si super().__new__(cls)renvoie une instance préexistante - normalement, cela sauterait __init__, mais pas votre code.
Eric
Hmm, selon la object.__new__documentation, __init__ne devrait être invoqué que si isinstance(instance, cls)? Cela me semble quelque peu flou ... Mais je ne vois la sémantique que vous revendiquez nulle part ...
khazhyk
En y réfléchissant davantage, si vous remplacez __new__pour retourner un objet préexistant, ce nouveau devrait être le plus extérieur pour avoir un sens, car d'autres implémentations de __new__n'auraient aucun moyen général de savoir si vous retournez une nouvelle instance non initialisée ou ne pas.
khazhyk
1
@khazhyk Eh bien, il y a définitivement quelque chose qui vous empêche de définir async def __init__(...), comme le montre l'OP, et je crois que cette TypeError: __init__() should return None, not 'coroutine'exception est codée en dur dans Python et ne peut pas être contournée. J'ai donc essayé de comprendre comment un async def __new__(...)faisait une différence. Maintenant, je crois comprendre que votre async def __new__(...)(ab) utilise la caractéristique de "si __new__()ne renvoie pas une instance de cls, alors __init__()ne sera pas invoqué". Votre nouveau __new__()retourne une coroutine, pas un cls. C'est pourquoi. Astuce astucieuse!
RayLuo
20

Je recommanderais une méthode d'usine distincte. C'est sûr et simple. Cependant, si vous insistez sur une asyncversion de __init__(), voici un exemple:

def asyncinit(cls):
    __new__ = cls.__new__

    async def init(obj, *arg, **kwarg):
        await obj.__init__(*arg, **kwarg)
        return obj

    def new(cls, *arg, **kwarg):
        obj = __new__(cls, *arg, **kwarg)
        coro = init(obj, *arg, **kwarg)
        #coro.__init__ = lambda *_1, **_2: None
        return coro

    cls.__new__ = new
    return cls

Usage:

@asyncinit
class Foo(object):
    def __new__(cls):
        '''Do nothing. Just for test purpose.'''
        print(cls)
        return super().__new__(cls)

    async def __init__(self):
        self.initialized = True

async def f():
    print((await Foo()).initialized)

loop = asyncio.get_event_loop()
loop.run_until_complete(f())

Production:

<class '__main__.Foo'>
True

Explication:

Votre construction de classe doit renvoyer un coroutineobjet au lieu de sa propre instance.

Huazuo Gao
la source
Ne pourriez-vous pas nommer votre new __new__et utiliser super(de même pour __init__, c'est-à-dire laisser simplement le client remplacer cela) à la place?
Matthias Urlichs
7

Mieux encore, vous pouvez faire quelque chose comme ça, ce qui est très simple:

import asyncio

class Foo:
    def __init__(self, settings):
        self.settings = settings

    async def async_init(self):
        await create_pool(dsn)

    def __await__(self):
        return self.async_init().__await__()

loop = asyncio.get_event_loop()
foo = loop.run_until_complete(Foo(settings))

Fondamentalement, ce qui se passe ici est __init__() appelé en premier, comme d'habitude. Puis __await__()est appelé qui attend ensuite async_init().

Vishnu shettigar
la source
3

[Presque] réponse canonique de @ojii

@dataclass
class Foo:
    settings: Settings
    pool: Pool

    @classmethod
    async def create(cls, settings: Settings, dsn):
        return cls(settings, await create_pool(dsn))
Dima Tisnek
la source
3
dataclassespour la victoire! si facile.
grisaitis