Python 异步请求+写文件混乱

Python async requests + writing files confusion

我有一个简单的 python 异步程序,可以从特定的 URL 检索 gzip 文件。我已经将 aiohttp 用于异步请求。根据 aiohttp 文档 (https://docs.aiohttp.org/en/stable/client_quickstart.html),我在我的测试方法中使用了他们在 'Streaming Response Content' 下的示例来写入数据。

async def main(url):
    async with aiohttp.ClientSession() as session:
        await asyncio.gather(test(session, url))


async def test(session, url):
    async with session.get(url=url) as r:
        with open('test.csv.gz', 'wb') as f:
            async for chunk in r.content.iter_chunked(1024):
                f.write(chunk)

但是,我不确定test() 中的内容是否实际上是异步的。我读过的许多文章都提到了 'await' 关键字在异步协程中激活异步性的要求(例如 r = await session.get(url=url)) ,但我想知道 'async with' 和 'async for' 模式是否也实现了相同的目的?

我希望实现的是在执行 session.get() 时以及将数据写入本地时的异步功能,这样如果我传入许多 urls 它将) 在获取 url 时执行异步切换和 b) 在将数据写入本地时执行异步切换。

对于 b),我需要使用类似下面的东西吗?

async with aiofiles.open('test.csv.gz', 'wb') as f:
    async for chunk in r.content.iter_chunked(1024):
        await f.write()

这让我想到了一个有点离题的问题,但是 async with session.get(url=url) as r:r = await session.get(url=url) 之间有什么区别?

请让我知道我的理解是否有缺陷,或者我是否缺少关于异步功能的一些基本知识!

好问题。看下面的小程序,它运行两个任务。每个都有一个异步上下文管理器和一个异步迭代器:

import asyncio
from contextlib import asynccontextmanager

@asynccontextmanager
async def nada():
    # await asyncio.sleep(0.0)
    yield
    
async def aloop():
    for _ in range(5):
        # await asyncio.sleep(0.0)
        yield
    
async def atask(name):
    async for _ in aloop():
        async with nada():
            print("Task", name)

async def main():
    asyncio.create_task(atask("1"))
    await asyncio.create_task(atask("2"))

if __name__ == "__main__":
    asyncio.run(main())

输出:

Task 1
Task 1
Task 1
Task 1
Task 1
Task 2
Task 2
Task 2
Task 2
Task 2

没有发生任务切换。

现在取消注释上下文管理器 (nada) 或迭代器 (aloop) 中的 await asyncio.sleep(0.0)。输出变为:

Task 1
Task 2
Task 1
Task 2
Task 1
Task 2
Task 1
Task 2
Task 1
Task 2

现在确实发生了任务切换。但是你的主程序在这两种情况下是完全一样的。

所以第一个问题的答案是 'async with' 和 'async for' 模式 不一定 导致任务切换。这取决于它们的实施。异步上下文管理器和异步迭代器都调用特殊机制;如果该机器执行 await 表达式,就会发生任务切换。但是 Python 不需要异步上下文管理器或异步迭代器来执行此操作。

这是完全合法的Python:

async def do_nothing:
    pass

实际上,您可能可以信任像 aiohttp 这样广泛部署的库。将 async 关键字放在方法前面并且 not 在其中执行 await 并没有多大价值。我能想到的唯一 use-case 是当 API 需要协程但您不需要异步行为时。将这种功能放在 general-use 库中将是糟糕的设计,无论如何不是没有好的文档。

你的第二个问题 - async with session.get(url=url) as r: 和 r = await session.get(url=url) 之间的区别是什么 - 第一种形式执行两个特殊函数而第二种形式不执行。第一个有点等同于:

try:
    x = session.get(url=url)
    r = await x.__aenter__()    
    # the indented block of code executes here
finally:
    await x.__aexit__(...)

__aexit__ 方法接受一些与异常处理有关的参数,您可以在文档中阅读这些参数。同步上下文管理器类似,只是特殊方法被命名为 __enter____exit__.