在Python中,'async for x in async_iterator'和'for x in await async_iterator'之间的区别是什么?

bhmjp9jg  于 2023-02-14  发布在  Python
关注(0)|答案(2)|浏览(155)

这个主题包含了整个想法。我看到了一个代码示例,它显示了如下内容:

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

以及其他代码为:

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

这两种方法是否有显著差异?

wrrgggsh

wrrgggsh1#

靶区; DR

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

不同的协议

可迭代

要理解这种区别,首先需要理解可迭代的概念。
抽象地说,如果一个对象实现了__iter__方法或(迭代中不太常见的)类似序列的__getitem__方法,那么它就是可迭代的
实际上,一个对象是iterable,如果你可以在for-循环中使用它,那么for _ in iterable.for-循环隐式调用iterable的__iter__方法,并期望它返回一个iterator,它实现了__next__方法,该方法在for-循环中每次迭代开始时调用,其返回值是分配给循环变量的值。

异步可迭代

async-world引入了它的一个变体,即异步可迭代
如果一个对象实现了__aiter__方法,那么它是异步可迭代的
同样,实际上讲,一个对象是异步可迭代的,如果它可以在async for-循环中使用,那么async for _ in async_iterable.async for-循环调用异步可迭代对象的__aiter__方法,并期望它返回一个异步迭代器,它实现了__anext__协程方法。该方法在async for循环的每次迭代开始时等待。

等待

一般来说,异步可迭代对象是不可等待的,也就是说,它不是一个协程,它不实现__await__方法,反之亦然。尽管它们不一定是互斥的。你可以设计一个对象,它本身是可等待的,也是(异步)可迭代的,尽管这看起来是一个非常奇怪的设计。

(异步)迭代器

为了使所使用的术语更加清楚,迭代器tor是迭代器ble的子类型。这意味着迭代器还通过提供__iter__方法来实现可迭代协议,但它也提供__next__方法。类似地,异步迭代器是异步可迭代的子类型,因为它实现了__aiter__方法。而且提供了__anext__协程方法。
你不需要这个对象是一个迭代器,就可以在for循环中使用它,你需要它来 * return * 一个迭代器。事实上你可以使用(异步)迭代器(asyncfor-循环是因为它也是一个(异步)可迭代的。很少有东西是可迭代的,但 * 不是 * 迭代器。在大多数情况下,对象将是两者(即后者)。

从你的例子中推断

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 * 实际上是一个协程函数(即返回一个可等待的可调用函数),并且该协程的返回值是一个正常的可迭代函数。
注意我们知道get_items协程返回的对象是一个 * normal * 可迭代对象,否则常规的for-循环将无法使用它。

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

这些代码片段的另一个含义是,在第一个中,函数(返回异步迭代器)是非异步的,也就是说,调用它不会产生对事件循环的控制,而async for-循环的每个迭代都是异步的(因此将允许上下文切换)。
相反,在第二个例子中,返回普通迭代器的函数是一个异步调用,但是所有的迭代(对__next__的调用)都是非异步的。

密钥差异

实际上,你展示的这两个代码片段是 * 从不 * 等价的。主要原因是get_items要么是要么不是协程函数。如果它不是 *,你就不能执行await get_items()。但是你是否能执行async forfor取决于get_items返回的内容。

可能的组合

为了完整起见,应该注意到上述协议的组合是完全可行的,尽管不是太常见。

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异常。

mlmc2os5

mlmc2os52#

那是完全不同的。
如果getItems()是一个异步迭代器或异步生成器,那么for item in await getItems()将不起作用(将抛出一个错误),只有当getItems是一个协程时才可以使用它,在您的情况下,协程应该返回一个序列对象(简单可迭代)。
async for是用于在 async 迭代器/生成器上进行异步迭代的常规(和Python)方式。

相关问题