Como definir o atributo de classe com await em __init__


95

Como posso definir uma classe com await no construtor ou corpo da classe?

Por exemplo, o que eu quero:

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 exemplo com atributo de corpo 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)

Minha solução (mas gostaria de ver uma forma mais elegante)

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

1
Você pode ter alguma sorte com __new__, embora possa não ser elegante
JBernardo

Não tenho experiência com o 3.5 e, em outras linguagens, isso não funcionaria devido à natureza viral de async / await, mas você já tentou definir uma função async como _pool_init(dsn)e depois chamá-la de __init__? Isso preservaria a aparência do init-no-construtor.
ap


1
use @classmethod😎 é um construtor alternativo. coloque o trabalho assíncrono lá; em seguida __init__, basta definir os selfatributos
grisaitis

Respostas:


120

A maioria dos métodos mágicos não são projetados para trabalhar com async def/ await- em geral, você só deve estar usando awaitdentro dos métodos mágicos assíncronos dedicados - __aiter__, __anext__, __aenter__, e __aexit__. Usá-lo dentro de outros métodos mágicos não funcionará, como é o caso com __init__(a menos que você use alguns truques descritos em outras respostas aqui), ou irá forçá-lo a sempre usar o que quer que acione a chamada do método mágico em um contexto assíncrono.

asyncioBibliotecas existentes tendem a lidar com isso de duas maneiras: primeiro, eu vi o padrão de fábrica usado ( asyncio-redispor exemplo):

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)

Outras bibliotecas usam uma função de co-rotina de nível superior que cria o objeto, em vez de um método de fábrica:

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)

A create_poolfunção aiopgque você deseja chamar __init__está, na verdade, usando esse padrão exato.

Isso pelo menos resolve o __init__problema. Eu não vi variáveis ​​de classe que fazem chamadas assíncronas que eu possa me lembrar, então não sei se surgiram padrões bem estabelecidos.


38

Outra maneira de fazer isso, por diversão:

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

2
Esta é atualmente a implementação mais clara e compreensível em minha opinião. Eu realmente gosto de como ele é intuitivamente extensível. Eu estava preocupado com a necessidade de me aprofundar nas metaclasses.
Tankobot

1
Isso não tem __init__semântica correta se super().__new__(cls)retorna uma instância pré-existente - normalmente, isso iria pular __init__, mas seu código não.
Eric

Hmm, por object.__new__documentação, __init__só deve ser invocado se isinstance(instance, cls)? Isso parece um pouco confuso para mim ... Mas eu não vejo a semântica que você afirma em lugar nenhum ...
khazhyk

Pensando mais nisso, se você substituir __new__para retornar um objeto pré-existente, esse novo precisaria ser o mais externo para fazer algum sentido, já que outras implementações de __new__não teriam uma maneira geral de saber se você está retornando uma nova instância não inicializada ou não.
khazhyk

1
@khazhyk Bem, definitivamente HÁ algo impedindo você de definir async def __init__(...), como mostrado pelo OP, e eu acredito que essa TypeError: __init__() should return None, not 'coroutine'exceção está codificada dentro do Python e não pode ser contornada. Tentei entender como isso async def __new__(...)fazia diferença. Agora, meu entendimento é que, seu async def __new__(...)(ab) use a característica de "se __new__()não retornar uma instância de cls, então __init__()não será invocado". Seu novo __new__()retorna uma co-rotina, não um cls. É por isso. Hack inteligente!
RayLuo

20

Eu recomendaria um método de fábrica separado. É seguro e simples. No entanto, se você insiste em uma asyncversão de __init__(), aqui está um exemplo:

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

Uso:

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

Resultado:

<class '__main__.Foo'>
True

Explicação:

A construção de sua classe deve retornar um coroutineobjeto em vez de sua própria instância.


Você não poderia nomear seu new __new__e usar super(da mesma forma para __init__, ou seja, apenas deixar o cliente substituir isso)?
Matthias Urlichs,

8

Melhor ainda, você pode fazer algo assim, o que é muito fácil:

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

Basicamente, o que acontece aqui é __init__() chamado primeiro, como de costume. Em seguida, __await__()é chamado e aguarda async_init().


3

Resposta [quase] canônica por @ojii

@dataclass
class Foo:
    settings: Settings
    pool: Pool

    @classmethod
    async def create(cls, settings: Settings, dsn):
        return cls(settings, await create_pool(dsn))

3
dataclassespara a vitória! tão fácil.
grisaitis

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.