Как сделать что-то похожее на обратные вызовы Javascript в Python - PullRequest
1 голос
/ 06 октября 2019

Я пытаюсь разобраться в Python asyncio. Это простая программа, которую я написал. Логика, которую я пытаюсь смоделировать, такова:

  1. Я получаю список имен из некоторой базы данных. Поскольку мы собираемся что-то сделать с этими именами после того, как мы их получим, следовательно, я сделал ее простой, а не асинхронной.

  2. После того, как мы получим данные, мы снова вызовемкакой-то внешний API, используя имя, которое у нас есть. Теперь, поскольку это будет дорогостоящей операцией с точки зрения ввода-вывода, а вызовы API для отдельных имен не зависят друг от друга, имеет смысл сделать их анонимными.

Я посмотрел эту темув Stackoverflow ( Кооперативный выход в asyncio ), который говорит, что для возврата управления циклу событий, чтобы сделать что-то еще, мы должны сделать asyncio.sleep(0).

Здесь я сравниваю асинхронное поведениеNode.js и Python. Если я верну управление циклу событий, используя приведенный выше синтаксис, мой длительный вызов API останется приостановленным и не будет происходить в фоновом режиме, как в Node.js?

В Node.js, когда мы создаем внешнийПри вызове API мы получаем что-то обратно под названием Promises, на котором мы можем дождаться завершения. По сути, это означает, что вызов базы данных или вызов API происходит в фоновом режиме, и мы возвращаем что-то, когда это делается.

Я что-то упускаю здесь что-то критическое для асинхронного программирования Python? Пожалуйста, проясните это немного подробнее.

Ниже приведен код и его вывод:

import asyncio
import time


async def get_message_from_api(name):
    # This is supposed to be a long running operation like getting data from external API
    print(f"Attempting to yield control to the other tasks....for {name}")
    await asyncio.sleep(0)
    time.sleep(2)
    return f"Creating message for {name}"


async def simulate_long_ops(name):
    print(f"Long running operation starting for {name}")
    message = await get_message_from_api(name)
    print(f"The message returned by the long running operation is {message}")


def get_data_from_database():
    return ["John", "Mary", "Sansa", "Tyrion"]


async def main():
    names = get_data_from_database()
    futures = []
    for name in names:
        futures.append(loop.create_task(simulate_long_ops(name)))
    await asyncio.wait(futures)


if __name__ == '__main__':
    try:
        loop = asyncio.get_event_loop()
        loop.run_until_complete(main())
    except Exception as e:
        print(e)
    finally:
        loop.close()

Вывод:

Long running operation starting for John
Attempting to yield control to the other tasks....for John
Long running operation starting for Mary
Attempting to yield control to the other tasks....for Mary
Long running operation starting for Sansa
Attempting to yield control to the other tasks....for Sansa
Long running operation starting for Tyrion
Attempting to yield control to the other tasks....for Tyrion
The message returned by the long running operation is Creating message for John
The message returned by the long running operation is Creating message for Mary
The message returned by the long running operation is Creating message for Sansa
The message returned by the long running operation is Creating message for Tyrion

1 Ответ

1 голос
/ 07 октября 2019

Ошибка в вашем коде в том, что вы звоните time.sleep. Вы никогда не должны вызывать эту функцию в асинхронном коде, она блокирует весь цикл событий;await asyncio.sleep() вместо. С точки зрения JavaScript, вызов time.sleep почти так же плох, как и сон как этот вместо как этот . (Я говорю «почти», потому что time.sleep по крайней мере не сжигает циклы ЦП во время ожидания.)

Попытки обойти эту ошибку привели ко второй проблеме, использованию asyncio.sleep(0) для предоставления контроляцикл событий. Хотя идиома была добавлена ​​рано, поведение было задокументировано только значительно позже . Как Guido намекает в исходном выпуске, явная уступка в цикле событий подходит только для расширенного использования, и его использование новичками, скорее всего, является ошибкой. Если ваша длительная операция асинхронная - как в вашем коде, после замены time.sleep() на await asyncio.sleep() - вам не нужно переходить в цикл обработки событий вручную. Вместо этого асинхронная операция будет сбрасываться по мере необходимости на каждом await, как это было бы в JavaScript.

В Node.js, когда мы выполняем внешний вызов API, мы получаем что-то обратно, называемое Promises, на котороммы можем дождаться окончания.

В Python future является близким аналогом, и асинхронные модели очень похожи. Одно существенное отличие состоит в том, что асинхронные функции Python возвращают не запланированные фьючерсы, а легкие сопрограммные объекты, которые вы должны либо дождаться, либо передать asyncio.create_task(), чтобы запустить их. Поскольку ваш код выполняет последнее, он выглядит правильно.

Возвращаемое значение create_task равно объекту , который реализует интерфейс Future. Будущие виды спорта - метод add_done_callback с семантикой, которую вы ожидаете. Но гораздо лучше вместо этого просто await будущее - это делает код более читабельным, и становится ясно, куда идут исключения.

Кроме того, вы, вероятно, хотите скорее использовать asyncio.gather()чем asyncio.wait(), чтобы исключения не остались незамеченными. Если вы используете Python 3.7, попробуйте использовать asyncio.run() для запуска основной функции async.

...