Python3中的Futures和ES6中的Promises之间的区别

41
自Python 3.5以来,引入了关键字awaitasync。我更喜欢使用Python 2.7,并且一直避免使用Python 3,因此asyncio对我来说是比较新的。从我的理解来看,await/async的工作方式与ES6(或JavaScript,ES2015,无论你想叫它什么)非常相似。

以下是我编写的两个脚本进行比较。

import asyncio

async def countdown(n):
    while n > 0:
        print(n)
        n -= 1
        await asyncio.sleep(1)

async def main():
    """Main, executed in an event loop"""

    # Creates two countdowns
    futures = asyncio.gather(
        countdown(3), 
        countdown(2)
    )
    # Wait for all of them to finish
    await futures
    # Exit the app
    loop.stop()

loop = asyncio.get_event_loop()
asyncio.ensure_future(main())
loop.run_forever()
function sleep(n){
    // ES6 does not provide native sleep method with promise support
    return new Promise(res => setTimeout(res, n * 1000));
}

async function countdown(n){
    while(n > 0){
        console.log(n);
        n -= 1;
        await sleep(1);
    }
}

async function main(){
    // Creates two promises
    var promises = Promise.all([
        countdown(3),
        countdown(2)
    ]);
    // Wait for all of them to finish
    await promises;
    // Cannot stop interpreter's event loop
}

main();

需要注意的一点是,这些代码非常相似并且工作方式基本相同。

以下是问题:

  1. 在Python和ES6中,await/async都是基于生成器的。认为Futures与Promises相同是否正确?

  2. 我在asyncio文档中看到了术语TaskFutureCoroutine。它们之间有什么区别?

  3. 我应该开始编写始终具有运行事件循环的Python代码吗?

2个回答

64
在Python和ES6中,await/async都基于生成器。认为Futures和Promises是相同的吗?
不是Future,而是Python的Task与Javascript的Promise大致相当。请参阅下面的详细信息。
asyncio文档中,我看到了术语TaskFutureCoroutine。它们之间有什么区别呢?
它们是非常不同的概念。主要是,TaskFutureCoroutine组成。让我们简要地描述一下这些原语(我将简化许多事情,只描述主要原则): Future Future只是一个值的抽象,可能尚未计算,并且最终将可用。它是一个简单的容器,只做一件事 - 每当设置值时,触发所有已注册的回调。
如果您想获取该值,可以通过add_done_callback()方法注册回调。
但与Promise不同,实际计算是在外部完成的 - 并且外部代码必须调用set_result()方法来解决未来。 Coroutine Coroutine是非常类似于Generator的对象。

生成器通常在for循环中迭代。它产生值,并且自PEP342被接受后,还可以接收值。

协程通常在asyncio库的事件循环深度内迭代。协程产生Future实例。当您迭代协程并且它产生一个future时,您应该等待直到此future已解析。然后,您应将future的值send到协程中,接着再接收下一个future,以此类推。

await表达式与yield from表达式几乎相同,因此通过等待其他协程,您会一直停留,直到该协程的所有future都已解决,并获取协程的返回值。 Future是一个单次可迭代的对象,其迭代器返回实际的Future - 这意味着await future等同于yield from future等同于yield future

Task

任务(Task)是已经开始计算并附加到事件循环的Future。因此,它是一种特殊类型的Future(类Task派生自类Future),它与某些事件循环相关联,并且具有作为任务执行器的一些协程。

任务通常由事件循环对象创建:您将协程提供给循环,它创建任务对象并开始以上述方式迭代该协程。一旦协程完成,任务的 Future 将由协程的返回值解析。

可以看出,任务与 JS Promise 非常相似 - 它封装了后台作业及其结果。

协程函数和异步函数

协程函数是协程的工厂,就像生成器函数是生成器的工厂一样。请注意 Python 协程函数和 Javascript 异步函数之间的区别 - 当调用 JS 异步函数时,它会创建一个 Promise 并立即开始迭代其内部生成器,而 Python 的协程在创建 Task 之前不会执行任何操作。

  1. 我应该编写始终运行事件循环的 Python 代码吗?

如果需要任何 asyncio 功能,则应该这样做。事实证明,混合同步和异步代码非常困难 - 最好让整个程序都是异步的(但是您可以通过 asyncio 线程池 API 在单独的线程中启动同步代码块)。


这很难理解,但这是我迄今为止想出的最好的解释。 - nadapez

1

我认为主要的区别在于下游。

const promise = new Promise((resolve, reject) => sendRequest(resolve, reject));
await promise;

在JavaScript中,resolvereject函数是由JS引擎创建的,它们必须被传递才能跟踪它们。最终,大多数情况下仍然使用两个回调函数,而Promise实际上不会比setTimeout(() => doMoreStuff())更多地做任何事情,只是在doStuff调用resolve后。一旦调用了回调函数,就无法检索旧结果或Promise的状态。Promise主要是常规调用和async/await之间的粘合代码(因此您可以在其他地方等待Promise),以及一些错误回调转发以进行链接.then

future = asyncio.Future()
sendRequest(future)
await future

在Python中,Future本身成为了接口,用于返回结果并跟踪结果。由于Andril已经给出了最接近JavaScript Promise(即Task;您提供一个回调函数并等待其完成)的Python等效方式,因此我想走另一条路线。
class Future {
  constructor() {
    this.result = undefined;
    this.exception = undefined;
    this.done = false;
    this.success = () => {};
    this.fail = () => {};
  }
  result() {
    if (!this.done) {
      throw Error("still pending");
    }
    return this.result();
  }
  exception() {
    if (!this.done) {
      throw Error("still pending");
    }
    return this.exception();
  }
  setResult(result) {
    if (this.done) {
      throw Error("Already done");
    }
    this.result = result;
    this.done = true;
    
    this.success(this.result);
  }
  setException(exception) {
    if (this.done) {
      throw Error("Already done");
    }
    this.exception = exception;
    this.done = true;

    this.fail(this.exception);
  }
  then(success, fail) {
    this.success = success;
    this.fail = fail;
  }
}

JS中的await基本上会生成两个回调函数,这些回调函数将传递给.then。在JS Promise中,实际逻辑应该在这里发生。在许多示例中,您将发现一个setTimeout(resolve, 10000)来演示跳出事件循环,但如果您改为跟踪这两个回调函数,则可以随意使用它们。

function doStuff(f) {
  // keep for some network traffic or so
  setTimeout(() => f.setResult(true), 3000);
}

const future = new Future();
doStuff(future);
console.log('still here');
console.log(await future);

上面的例子表明,'还在这里'三秒后你会得到'true'。

如你所见,Promise接受一个工作函数并在内部处理resolve和reject,而Future不内部化工作,只关心回调。个人而言,我更喜欢Future,因为它少了一层回调地狱——这也是Promises的首要原因之一:回调链。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接