在Python中,`async for x in async_iterator`和`for x in await async_iterator`有什么区别?

4

主题包含整个想法。我遇到了一个代码示例,其中显示了类似以下内容:

async for item in getItems():
    await item.process()

还有其他代码如下:

for item in await getItems():
    await item.process()

这两种方法有明显的区别吗?

2个回答

4

简而言之

虽然它们两者理论上可以使用相同的对象(不会导致错误),但它们很可能并不这样做。一般来说,这两种表示法根本不等价,而是调用完全不同的协议,并应用于非常不同的用例。


不同的协议

可迭代对象

要理解它们之间的区别,首先需要了解 可迭代对象 的概念。

抽象地说,如果一个对象实现了 __iter__ 方法或者(较少用于迭代)类似序列的 __getitem__ 方法,则该对象是可迭代的

实际上,如果您可以在for循环中使用对象,则该对象是可迭代的,因此for _ in iterablefor循环隐式调用可迭代对象的__iter__方法,并期望其返回一个迭代器,该迭代器实现__next__方法。该方法在for循环的每次迭代开始时被调用,其返回值是分配给循环变量的值。

异步可迭代对象

async世界引入了一种变体,即异步可迭代对象

如果一个对象实现了__aiter__方法,则该对象是异步可迭代的

从实际角度来看,如果一个对象可以在async for循环中使用,例如async for _ in async_iterable,那么它就是异步可迭代的async for循环调用异步可迭代的__aiter__方法,并期望它返回一个异步迭代器,该迭代器实现了__anext__协程方法。该方法在每次async for循环迭代开始时被等待。

可等待对象

一般来说,异步可迭代对象不是可等待的,也就是说它不是一个协程,也没有实现__await__方法,反之亦然。虽然它们不一定是互斥的。你可以设计一个既可以自己等待,又可以(异步)迭代的对象,但这似乎是一个非常奇怪的设计。

(异步)迭代器

为了在使用术语时非常清楚,迭代是迭代对象子类型。这意味着迭代器通过提供__iter__方法来实现可迭代协议,但它还提供了__next__方法。类似地,异步迭代器是异步可迭代对象的子类型,因为它实现了__aiter__方法,但也提供了__anext__协程方法。

你不需要对象成为迭代器就能在 for 循环中使用它,你需要的是它返回一个迭代器。你可以在 (async) for 循环中使用 (异步) 迭代器的原因是它同时也是 (异步) 可迭代的。一个可迭代但不是迭代器的情况很少见。在大多数情况下,对象将同时是两者(即后者)。


从你的例子中推断

async for _ in get_items()

这段代码意味着get_items函数返回的任何内容都是异步可迭代对象。

请注意,get_items只是一个普通的非async函数,但它返回的对象实现了异步可迭代协议。这意味着我们可以写成以下形式:

async_iterable = get_items()
async for item in async_iterable:
    ...

for _ in await get_items()

这段代码表明get_items实际上是一个协程函数(即可调用的返回awaitable的函数),而该协程的返回值是一个普通的可迭代对象。

请注意,我们可以确定get_items协程返回的对象是一个普通的可迭代对象,否则常规的for循环将无法使用它。等价的代码如下:

iterable = await get_items()
for item in iterable:
    ...

影响

这些代码片段的另一个影响是,在第一个代码片段中,函数(返回异步迭代器)是非异步的,即调用它不会将控制权交给事件循环,而每个async for-loop的迭代都是异步的(因此允许上下文切换)。

相反,在第二个代码片段中,返回普通迭代器的函数是一个异步调用,但所有的迭代(对__next__的调用)都是非异步的

关键区别

实际应用的结论应该是,你展示的这两个代码片段永远不会等价。主要原因是get_items要么是协程函数,要么不是。如果它不是,你就不能执行await get_items()。但是,无论get_items返回什么,你是否可以执行async forfor都取决于它。


可能的组合

为了完整起见,值得注意的是,上述协议的组合是完全可行的,尽管并不是很常见。考虑以下示例:

from __future__ import annotations

class Foo:
    x = 0

    def __iter__(self) -> Foo:
        return self

    def __next__(self) -> int:
        if self.x >= 2:
            raise StopIteration
        self.x += 1
        return self.x

    def __aiter__(self) -> Foo:
        return self

    async def __anext__(self) -> int:
        if self.x >= 3:
            raise StopAsyncIteration
        self.x += 1
        return self.x * 10


async def main() -> None:
    for i in Foo():
        print(i)
    async for i in Foo():
        print(i)

if __name__ == "__main__":
    from asyncio import run
    run(main())

在这个例子中,Foo 实现了四个不同的协议:
  • 可迭代协议(def __iter__
  • 迭代器协议(可迭代 + def __next__
  • 异步可迭代协议(def __aiter__
  • 异步迭代器协议(异步可迭代 + async def __anext__
运行 main 协程将输出以下内容:
1
2
10
20
30

这说明对象在同一时间内可以同时具备这些特征。由于Foo既是同步的,又是异步可迭代的,我们可以编写两个函数——一个协程,一个普通函数——每个函数都返回Foo的一个实例,然后稍微复制你的示例:
from collections.abc import AsyncIterable, Iterable


def get_items_sync() -> AsyncIterable[int]:
    return Foo()


async def get_items_async() -> Iterable[int]:
    return Foo()


async def main() -> None:
    async for i in get_items_sync():
        print(i)
    for i in await get_items_async():
        print(i)
    async for i in await get_items_async():
        print(i)


if __name__ == "__main__":
    from asyncio import run
    run(main())

输出:

10
20
30
1
2
10
20
30

这非常清楚地说明了决定调用我们的Foo方法(__next____anext__)的唯一因素是我们使用for循环还是async for循环。

for循环将始终至少调用一次__next__方法,并在每次迭代时继续调用它,直到拦截到StopIteration异常。

async for循环将始终至少等待一次__anext__协程,并在每个后续迭代中继续调用和等待它,直到拦截到StopAsyncIteration异常。


0

它们是完全不同的。
如果getItems()是异步迭代器或异步生成器,那么这个for item in await getItems()将无法工作(会抛出错误),它只能用于getItems是协程的情况下,而在您的情况下,它应该返回一个序列对象(简单可迭代对象)。
async for是一种传统的(也是Pythonic的)异步迭代器/生成器的方式。


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