So setzen Sie das Klassenattribut mit await in __init__

94

Wie kann ich eine Klasse mit awaitim Konstruktor oder im Klassenkörper definieren?

Zum Beispiel was ich will:

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'

oder Beispiel mit Klassenkörperattribut:

class Foo(object):

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

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

foo = Foo(settings)

Meine Lösung (aber ich würde gerne einen eleganteren Weg sehen)

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
quelle
1
Sie könnten etwas Glück haben __new__, obwohl es vielleicht nicht elegant ist
JBernardo
Ich habe keine Erfahrung mit 3.5 und in anderen Sprachen würde dies aufgrund der viralen Natur von async / await nicht funktionieren, aber haben Sie versucht, eine asynchrone Funktion wie zu definieren _pool_init(dsn)und sie dann aufzurufen __init__? Es würde das Erscheinungsbild des Init-in-Konstruktors beibehalten.
ap
1
Wenn Sie curio verwenden: curio.readthedocs.io/en/latest/…
matsjoyce
1
benutze @classmethod😎 es ist ein alternativer Konstruktor. lege die asynchrone Arbeit dort hin; Dann __init__setzen Sie einfach die selfAttribute
Grisaitis

Antworten:

119

Die meisten magischen Methoden sind nicht zur Arbeit entwickelt , um mit async def/ await- in der Regel sollten Sie nur verwenden werden awaitinnerhalb der dedizierten asynchronen magischen Methoden - __aiter__, __anext__, __aenter__, und __aexit__. Die Verwendung in anderen magischen Methoden funktioniert entweder überhaupt nicht, wie dies der Fall ist __init__(es sei denn, Sie verwenden einige Tricks, die in anderen Antworten hier beschrieben sind), oder zwingt Sie, immer das zu verwenden, was den Aufruf der magischen Methode in einem asynchronen Kontext auslöst.

Bestehende asyncioBibliotheken können dies auf zwei Arten behandeln: Erstens habe ich das verwendete Factory-Muster gesehen ( asyncio-rediszum Beispiel):

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)

Andere Bibliotheken verwenden eine Coroutine-Funktion der obersten Ebene, die das Objekt erstellt, und keine Factory-Methode:

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)

Die create_poolFunktion, von aiopgder aus Sie aufrufen möchten, __init__verwendet genau dieses Muster.

Dies behebt zumindest das __init__Problem. Ich habe keine Klassenvariablen gesehen, die asynchrone Aufrufe in freier Wildbahn ausführen, an die ich mich erinnern kann, daher weiß ich nicht, dass etablierte Muster entstanden sind.

dano
quelle
37

Ein anderer Weg, dies für Funsies zu tun:

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
quelle
2
Dies ist meiner Meinung nach derzeit die klarste und verständlichste Implementierung. Mir gefällt sehr, wie intuitiv erweiterbar es ist. Ich befürchtete, es wäre notwendig, sich mit Metaklassen zu beschäftigen.
Tankobot
1
Dies hat keine korrekte __init__Semantik, wenn super().__new__(cls)eine bereits vorhandene Instanz zurückgegeben wird. Normalerweise wird dies übersprungen __init__, Ihr Code jedoch nicht.
Eric
Hmm, laut object.__new__Dokumentation __init__sollte nur aufgerufen werden, wenn isinstance(instance, cls)? Dies scheint mir etwas unklar ... Aber ich sehe die Semantik, die Sie behaupten, nirgendwo ...
Khazhyk
Wenn Sie darüber nachdenken __new__, um ein bereits vorhandenes Objekt zurückzugeben, muss dieses neue das äußerste sein, um einen Sinn zu ergeben, da andere Implementierungen von __new__keine allgemeine Möglichkeit haben, zu wissen, ob Sie eine neue nicht initialisierte Instanz zurückgeben oder nicht.
Khazhyk
1
@khazhyk Nun, es gibt definitiv etwas, das Sie daran hindert, zu definieren async def __init__(...), wie das OP zeigt, und ich glaube, dass die TypeError: __init__() should return None, not 'coroutine'Ausnahme in Python fest codiert ist und nicht umgangen werden konnte. Also versuchte ich zu verstehen, wie async def __new__(...)es einen Unterschied machte. Mein Verständnis ist nun, dass Ihr async def __new__(...)(ab) das Merkmal "Wenn __new__()keine Instanz von cls zurückgegeben wird, __init__()wird es nicht aufgerufen" verwendet. Ihr neuer __new__()gibt eine Coroutine zurück, keine cls. Deshalb. Cleverer Hack!
RayLuo
20

Ich würde eine separate Factory-Methode empfehlen. Es ist sicher und unkompliziert. Wenn Sie jedoch auf einer asyncVersion von bestehen __init__(), finden Sie hier ein Beispiel:

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

Verwendung:

@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())

Ausgabe:

<class '__main__.Foo'>
True

Erläuterung:

Ihre Klassenkonstruktion muss ein coroutineObjekt anstelle einer eigenen Instanz zurückgeben.

Huazuo Gao
quelle
Könnten Sie nicht stattdessen Ihre benennen new __new__und verwenden super(ebenfalls für __init__, dh lassen Sie den Client dies einfach überschreiben)?
Matthias Urlichs
7

Besser noch, Sie können so etwas tun, was sehr einfach ist:

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))

Grundsätzlich wird hier __init__() wie gewohnt zuerst aufgerufen. Dann __await__()wird gerufen, was dann wartet async_init().

Vishnu Shettigar
quelle
3

[Fast] kanonische Antwort von @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
quelle
3
dataclassesfür den Sieg! so einfach.
Grisaitis