Сопрограммы в магических методах?

скажем, у меня есть волшебный метод в Python 3.6, например:

def __str__(self):
    return 'a {self.color} car'.format(self=self)

Теперь я хочу добавить в него сопрограмму, что-то вроде:

async def __str__(self):
    await self.my_coro(args)
    return 'a {self.color} car'.format(self=self)

Насколько я знаю, перед дефом нужно добавить асинхронность, но с магическими методами это похоже не работает. Есть ли (простой) обходной путь или он совершенно невозможен?


person zonk    schedule 30.01.2020    source источник
comment
Это потребует, чтобы все потребители (вызывающие) этого API обрабатывали его как асинхронный метод, который не определен в его поведении. Другими словами, никто не ожидает, что __str__ будет асинхронным, поэтому никто не будет await его использовать, так что это не сработает. На самом деле это не относится к магическим методам, вы будете иметь это каждый раз, когда будете реализовывать уже определенный интерфейс.   -  person deceze♦    schedule 30.01.2020
comment
Что вы пытаетесь достичь, в частности? В примере с __str__ нет необходимости использовать await для возврата отформатированной строки.   -  person mkrieger1    schedule 30.01.2020
comment
Не говоря уже о том, что такой метод, как __str__, не должен быть настолько сложным/дорогим, чтобы включать асинхронный вызов.   -  person deceze♦    schedule 30.01.2020
comment
@mkrieger1 mkrieger1 Я хотел заменить функцию синхронизации на сопрограмму в проекте. Но крысиный хвост действительно длинный (потому что теперь я должен делать операторы ожидания везде, где использовалась/используется старая функция/новая сопрограмма и т. д.), поэтому я пытаюсь заменить каждый метод синхронизации асинхронным методом, но мой тупик – это __exit__ Метод.   -  person zonk    schedule 30.01.2020
comment
См. PEP 492. .   -  person mkrieger1    schedule 30.01.2020
comment
@zonk Вы запрашиваете специальные методы в целом или контекстные менеджеры в частности?   -  person MisterMiyagi    schedule 30.01.2020
comment
@ mkrieger1 mkrieger1 tbh Я понятия не имею об асинхронных менеджерах контекста. Я посмотрю на это, если это может быть полезной подсказкой для проблемы.   -  person zonk    schedule 30.01.2020
comment
@MisterMiyagi В моем случае это особый метод, и я думаю, что применю и другие специальные методы, поэтому я бы сказал специальные методы в целом   -  person zonk    schedule 30.01.2020
comment
__enter__ и __exit__ используются для реализации менеджеров контекста. Если вы чувствуете необходимость использовать в них await, вам нужен асинхронный менеджер контекста, который вы можете реализовать либо путем замены этих методов на __aenter__ и __aexit__, либо с помощью одного из решений из вопроса, который я связал.   -  person mkrieger1    schedule 30.01.2020


Ответы (1)


TLDR: нет обходного пути, поскольку Python ожидает определенного типа для многих специальных методов. Однако некоторые специальные методы имеют асинхронные варианты.


Функция async def принципиально другого типа. Подобно тому, как функция генератора оценивается как генератор, функция сопрограммы оценивается как ожидаемое.

Вы можете определить специальные методы как асинхронные, но это повлияет на их возвращаемый тип. В случае __str__ вы получаете Awatable[str] вместо голого str. Поскольку Python требует здесь str, это приводит к ошибке.

>>> class AStr:
...     async def __str__(self):
...         return 'Hello Word'
>>> str(AStr())

TypeError: __str__ returned non-string (type coroutine)

Это влияет на все специальные методы, которые напрямую интерпретируются Python: к ним относятся __str__, __repr__, __bool__, __len__, __iter__, __enter__ и некоторые другие. Обычно, если специальный метод относится к некоторым внутренним функциям (например, str для прямого отображения) или оператору (например, for, требующему итератора), он не может быть async.


Некоторые специальные методы не интерпретируются Python напрямую. Примеры включают арифметические операторы (__add__, __sub__,...), сравнения (__lt__, __eq__,...) и поиск (__get__, __getattribute__,...). Их возвращаемый тип может быть любым объектом, включая ожидаемые.

Вы можете определить такие специальные методы через async def. Это влияет на их возвращаемый тип, но требует только клиентского кода для их await. Например, вы можете определить + как await (a + b).

>>> def AWAIT(awaitable):
...     """Basic event loop to allow synchronous ``await``"""
...     coro = awaitable.__await__()
...     try:
...         while True:
...             coro.send(None)
...     except StopIteration as e:
...         return e.args[0] if e.args else None
...
>>> class APlus:
...     def __init__(self, value):
...         self.value = value
...     async def __add__(self, other):
...         return self.value + other
...
>>> async def add(start, *values):
...     total = start
...     for avalue in map(APlus, values):
...         total = await (avalue + total)
...     return total
...
>>> AWAIT(add(5, 10, 42, 23))
80

Для механизма await существует несколько специальных методов, которые должны возвращать ожидаемое значение. Сюда входят __aenter__, __aexit__ и __anext__. Примечательно, что __await__ должен возвращать итератор, а не ожидаемое значение.

Вы можете (и в большинстве случаев должны) определить эти методы как async def. Если вам нужны асинхронные возможности в соответствующем специальном методе синхронизации, используйте соответствующий специальный асинхронный метод с async def. Например, вы можете определить асинхронный менеджер контекста.

>>> class AContext:
...     async def __aenter__(self):
...         print('async enter')
...     async def __aexit__(self, exc_type, exc_val, exc_tb):
...         print('async exit')
...
>>> async def scoped(message):
...     async with AContext():
...         print(message)
...
>>> AWAIT(scoped("Hello World"))
async enter
Hello World
async exit
person MisterMiyagi    schedule 30.01.2020