Як встановити атрибут класу з await у __init__


92

Як я можу визначити клас за awaitдопомогою конструктора чи тіла класу?

Наприклад, що я хочу:

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'

або приклад з атрибутом body class:

class Foo(object):

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

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

foo = Foo(settings)

Моє рішення (Але я хотів би бачити більш елегантний спосіб)

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
Можливо, вам пощастить __new__, хоча це може бути і не елегантно
ДжБернардо

У мене немає досвіду роботи з версією 3.5, а іншими мовами це не спрацювало б через вірусну природу async / await, але чи намагалися ви визначити асинхронну функцію типу, _pool_init(dsn)а потім викликати її з __init__? Це збереже зовнішній вигляд init-in-constructor.
ap

1
Якщо ви використовуєте curio: curio.readthedocs.io/en/latest/…
matsjoyce

1
використовуйте @classmethod😎 це альтернативний конструктор. помістіть туди роботу асинхронізації; потім __init__, просто встановіть selfатрибути
grisaitis

Відповіді:


117

Більшість магічних методи не призначені для роботи з async def/ await- в загальному, ви повинні використовувати тільки awaitвсередині виділені асинхронні методи магії - __aiter__, __anext__, __aenter__, і __aexit__. Використання його в інших магічних методах або взагалі не буде працювати, як це відбувається __init__(якщо ви не використовуєте деякі трюки, описані в інших відповідях тут), або змусить вас завжди використовувати те, що викликає виклик магічного методу в асинхронному контексті.

Існуючі asyncioбібліотеки, як правило, вирішують це одним із двох способів: По-перше, я бачив використаний заводський шаблон ( asyncio-redisнаприклад):

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)

В інших бібліотеках використовується функція супрограма верхнього рівня, яка створює об’єкт, а не фабричний метод:

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_poolФункція від aiopgякий ви хочете дзвонити в __init__насправді використовує саме цей шаблон.

Це принаймні вирішує __init__проблему. Я не бачив змінних класів, які здійснюють асинхронні виклики в дикій природі, які я можу згадати, тому я не знаю, що виникли якісь усталені закономірності.


36

Ще один спосіб зробити це для мешканців:

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
На даний момент це найбільш зрозуміле і зрозуміле імплементація. Мені дуже подобається, наскільки це інтуїтивно розширюване. Я переживав, що потрібно буде заглибитися в метакласи.
Tankobot

1
Це не має правильної __init__семантики, якщо super().__new__(cls)повертає вже існуючий екземпляр - зазвичай це буде пропущено __init__, але ваш код ні.
Ерік

Хм, відповідно до object.__new__документації, __init__слід посилатися лише якщо isinstance(instance, cls)? Мені це здається дещо незрозумілим ... Але я ніде не бачу семантики, на яку ви стверджуєте ...
khazhyk

Думаючи про це більше, якщо ви перевизначаєте __new__повернення вже існуючого об'єкта, цей новий повинен бути найзахищенішим, щоб мати якийсь сенс, оскільки інші реалізації __new__не матимуть загального способу дізнатися, чи повертаєте ви новий неініціалізований екземпляр або ні.
хажик

1
@khazhyk Ну, точно є щось, що заважає вам визначити async def __init__(...), як показав OP, і я вважаю, що TypeError: __init__() should return None, not 'coroutine'виняток жорстко закодований всередині Python і його не можна обійти. Тож я спробував зрозуміти, як це async def __new__(...)щось змінило. Зараз я розумію, що ви async def __new__(...)(ab) використовуєте характеристику "якщо __new__()не повертає екземпляр cls, тоді __init__()не буде викликано". Ваше нове __new__()повертає спільну програму, а не cls. Ось чому. Розумний хак!
RayLuo

20

Я б порекомендував окремий заводський метод. Це безпечно і просто. Однак, якщо ви наполягаєте на asyncверсії __init__(), ось приклад:

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

Використання:

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

Вихід:

<class '__main__.Foo'>
True

Пояснення:

Конструкція класу повинна повертати coroutineоб’єкт замість власного екземпляра.


Чи не могли б ви назвати new __new__і використати super(так само для __init__, тобто просто дозволити клієнту це замінити)?
Маттіас Урлікс

7

А ще краще, ви можете зробити щось подібне, що дуже просто:

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

В основному те, що тут відбувається, __init__() викликається спочатку, як зазвичай. Потім __await__()телефонує, що потім чекає async_init().


3

[Майже] канонічна відповідь @ojii

@dataclass
class Foo:
    settings: Settings
    pool: Pool

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

3
dataclassesза перемогу! так легко.
grisaitis
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.