Jak ustawić atrybut klasy z await w __init__

92

Jak mogę zdefiniować klasę awaitw konstruktorze lub treści klasy?

Na przykład to, czego chcę:

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'

lub przykład z atrybutem body body:

class Foo(object):

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

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

foo = Foo(settings)

Moje rozwiązanie (ale chciałbym zobaczyć bardziej elegancki sposób)

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
źródło
1
Możesz mieć trochę szczęścia __new__, chociaż może nie być eleganckie
JBernardo
Nie mam doświadczenia z 3.5, aw innych językach to by nie zadziałało z powodu wirusowej natury async / await, ale czy próbowałeś zdefiniować funkcję asynchroniczną, taką jak, _pool_init(dsn)a następnie wywołać ją z __init__? Zachowałoby to wygląd init-in-constructor.
AP
1
Jeśli używasz curio: curio.readthedocs.io/en/latest/ ...
matsjoyce
1
użyj @classmethod😎 jest to alternatywny konstruktor. umieść tam pracę asynchroniczną; następnie w __init__, po prostu ustaw selfatrybuty
grisaitis

Odpowiedzi:

117

Większość magicznych metod nie jest przeznaczona do pracy z async def/ await- generalnie powinieneś używać tylkoawait wewnątrz dedykowanych asynchronicznych metod magicznych - __aiter__, __anext__, __aenter__, i __aexit__. Używanie go w innych magicznych metodach albo w ogóle nie zadziała, jak to ma miejsce w przypadku __init__(chyba że użyjesz jakichś sztuczek opisanych w innych odpowiedziach tutaj), albo zmusi cię do używania wszystkiego, co wyzwala wywołanie metody magicznej w kontekście asynchronicznym.

Istniejący asyncio biblioteki zwykle radzą sobie z tym na dwa sposoby: Po pierwsze, widziałem używany wzorzec fabryki ( asyncio-redisna przykład):

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)

Inne biblioteki używają funkcji współrzędnych najwyższego poziomu, która tworzy obiekt, zamiast metody fabrycznej:

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)

create_poolFunkcja z aiopgktóry chcesz zadzwonić __init__faktycznie używając dokładnie ten wzór.

To przynajmniej rozwiązuje __init__problem. Nie widziałem zmiennych klas, które wykonują wywołania asynchroniczne w środowisku naturalnym, które mogę sobie przypomnieć, więc nie wiem, czy pojawiły się jakieś dobrze ugruntowane wzorce.

dano
źródło
35

Inny sposób na zrobienie tego, dla zabawy:

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
źródło
2
Moim zdaniem jest to obecnie najbardziej przejrzysta i zrozumiała realizacja. Bardzo podoba mi się to, jak intuicyjnie jest rozszerzalne. Martwiłem się, że konieczne będzie zagłębienie się w metaklasy.
Tankobot
1
To nie ma poprawnej __init__semantyki, jeśli super().__new__(cls)zwraca wcześniej istniejące wystąpienie - normalnie byłoby to pomijane __init__, ale twój kod nie.
Eric
Hmm, zgodnie z object.__new__dokumentacją __init__należy się powoływać tylko wtedy, gdy isinstance(instance, cls)? Wydaje mi się to trochę niejasne ... Ale nigdzie nie widzę semantyki, którą twierdzisz ...
khazhyk
Myśląc o tym więcej, jeśli przesłonisz, __new__aby zwrócić wcześniej istniejący obiekt, ten nowy musiałby być najbardziej zewnętrznym, aby miał jakikolwiek sens, ponieważ inne implementacje __new__nie miałyby ogólnego sposobu na sprawdzenie, czy zwracasz nową niezainicjowaną instancję lub nie.
khazhyk
1
@khazhyk Cóż, zdecydowanie JEST coś, co uniemożliwia ci zdefiniowanie async def __init__(...), jak pokazał OP, i uważam, że TypeError: __init__() should return None, not 'coroutine'wyjątek jest zakodowany na stałe w Pythonie i nie można go ominąć. Więc starałem się zrozumieć, jak to async def __new__(...)się zmieniło. Teraz rozumiem, że używasz async def __new__(...)(ab) cechy „jeśli __new__()nie zwróci instancji cls, to __init__()nie zostanie wywołane”. Twój nowy __new__()zwraca coroutine, a nie cls. Dlatego. Sprytny hack!
RayLuo
20

Poleciłbym osobną metodę fabryczną. To bezpieczne i proste. Jeśli jednak nalegasz na asyncwersję programu __init__(), oto przykład:

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

Stosowanie:

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

Wynik:

<class '__main__.Foo'>
True

Wyjaśnienie:

Twoja konstrukcja klasy musi zwracać coroutineobiekt zamiast własnej instancji.

Huazuo Gao
źródło
Czy nie mógłbyś nazwać swojego new __new__i użyć super(podobnie __init__, tj. Po prostu pozwolić klientowi to zastąpić)?
Matthias Urlichs
7

Jeszcze lepiej możesz zrobić coś takiego, co jest bardzo łatwe:

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

Zasadniczo to, co się tutaj dzieje, jest __init__() jak zwykle wywoływane jako pierwsze. Następnie __await__()zostaje wezwany, który następnie czeka async_init().

Vishnu shettigar
źródło
3

[Prawie] kanoniczna odpowiedź @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
źródło
3
dataclassesdla zwycięstwa! tak łatwo.
grisaitis