В Python 3.5+ я часто сталкиваюсь с ситуацией, когда у меня много вложенных сопрограмм только для вызова чего-то, что является глубоко сопрограммой, когда await просто входит в хвостовой вызов в большинстве функций, например, так: :

import asyncio

async def deep(time):
    await asyncio.sleep(time)
    return time

async def c(time):
    time *= 2
    return await deep(time)

async def b(time):
    time *= 2
    return await c(time)

async def a(time):
    time *= 2
    return await b(time)

async def test():
    print(await a(0.1))

loop = asyncio.get_event_loop()
loop.run_until_complete(test())
loop.close()

Эти функции a, b и c могут быть записаны как обычные функции, которые возвращают сопрограмму, а не как сами сопрограммы, следующим образом:

import asyncio

async def deep(time):
    await asyncio.sleep(time)
    return time

def c(time):
    time *= 2
    return deep(time)

def b(time):
    time *= 2
    return c(time)

def a(time):
    time *= 2
    return b(time)

async def test():
    print(await a(0.1))

loop = asyncio.get_event_loop()
loop.run_until_complete(test())
loop.close()

Какой путь более Pythonic? Какой способ более производительный? Какой путь другим будет легче поддерживать в будущем?

Редактировать - Измерение производительности

В качестве теста производительности я удалил строку await asyncio.sleep(time) из deep и рассчитал 1 000 000 итераций await a(0.1). В моей тестовой системе с CPython 3.5.2 первая версия заняла около 2,4 секунды, а вторая - около 1,6 секунды. Таким образом, похоже, что выполнение всех сопрограмм может привести к снижению производительности, но это определенно не на порядок. Возможно, кто-то с большим опытом профилирования кода Python мог бы создать надлежащий тест и окончательно решить проблему производительности.

6
JohnSpeeks 28 Май 2017 в 04:46

2 ответа

Лучший ответ

Используйте первое: вы не только явно показывает места, где код может быть приостановлен (где находится await), но также получаете все связанные с этим преимущества, такие как трассировки, которые показывают полезный поток выполнения.

Чтобы увидеть разницу, измените свою deep сопрограмму, чтобы выдать ошибку:

async def deep(time):
    await asyncio.sleep(time)
    raise ValueError('some error happened')
    return time

Для первого фрагмента вы увидите такой вывод:

Traceback (most recent call last):
  File ".\tmp.py", line 116, in <module>
    loop.run_until_complete(test())
  File ".\Python36\lib\asyncio\base_events.py", line 466, in run_until_complete
    return future.result()
  File ".\tmp.py", line 113, in test
    print(await a(0.1))
  File ".\tmp.py", line 110, in a
    return await b(time)
  File ".\tmp.py", line 106, in b
    return await c(time)
  File ".\tmp.py", line 102, in c
    return await deep(time)
  File ".\tmp.py", line 97, in deep
    raise ValueError('some error happened')
ValueError: some error happened

Но только для второго фрагмента:

Traceback (most recent call last):
  File ".\tmp.py", line 149, in <module>
    loop.run_until_complete(test())
  File ".\Python36\lib\asyncio\base_events.py", line 466, in run_until_complete
    return future.result()
  File ".\tmp.py", line 146, in test
    print(await a(0.1))
  File ".\tmp.py", line 130, in deep
    raise ValueError('some error happened')
ValueError: some error happened

Как видите, первая трассировка помогает увидеть «реальный» (и полезный) поток выполнения, а вторая - нет.

Первый способ написания кода также гораздо лучше поддерживать: представьте, что вы однажды поняли, что b(time) также должен содержать некоторые асинхронные вызовы, такие как await asyncio.sleep(time). В первом фрагменте этот вызов может быть выполнен напрямую без каких-либо других изменений, но во втором вам придется переписать многие части вашего кода.

5
Mikhail Gerasimov 28 Май 2017 в 09:15

Это один из редких случаев, когда "это Pythonic?" на самом деле не вопрос, основанный на мнении. Оптимизация вызова на хвосте официально не является Pythonic:

Итак, позвольте мне защитить свою позицию (то есть, я не хочу, чтобы [устранение хвостовой рекурсии] в языке). Если вам нужен короткий ответ, он просто непифоничен - BDFL

( см. также)

1
Nathaniel J. Smith 30 Май 2017 в 05:43