Как установить атрибут класса с ожиданием в __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'

или пример с атрибутом тела класса:

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__, хотя это может быть не изящно
JBernardo
У меня нет опыта работы с 3.5, и на других языках это не сработает из-за вирусной природы async / await, но пробовали ли вы определить функцию async, например, _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__проблему. Я не видел переменных класса, которые делают асинхронные вызовы в естественных условиях, которые я могу припомнить, поэтому я не знаю, возникли ли какие-либо устойчивые шаблоны.

дано
источник
35

Другой способ сделать это для фанатов:

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)? Мне это кажется несколько непонятным ... Но я нигде не вижу семантики, о которой вы говорите ...
хажик
Если подумать об этом больше, если вы переопределите, __new__чтобы вернуть уже существующий объект, этот новый должен быть самым внешним, чтобы иметь какой-либо смысл, поскольку другие реализации не __new__будут иметь общего способа узнать, возвращаете ли вы новый неинициализированный экземпляр или не.
khazhyk
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