为什么我们需要`async for`和`async with`?

nal*_*zok 5 python asynchronous coroutine async-await python-asyncio

引入async for和的意义async with何在?我知道这些陈述有 PEP,但它们显然是为语言设计者准备的,而不是像我这样的普通用户。将不胜感激补充示例的高级理由。

我自己做了一些研究并找到了这个答案

async forasync with语句必要的,因为你会打破yield from/await与裸链forwith报表。

作者没有举例说明链条是如何断裂的,所以我仍然很困惑。此外,我注意到 Python 有async forand async with,但没有async whileand async try ... except。这听起来很奇怪,因为和分别是for和 的with语法糖。我的意思是,考虑到后者是前者的构建块,它们的版本不会允许更大的灵活性吗?whiletry ... exceptasync

还有另一种答案讨论async for,但它仅覆盖它是什么并不对,并没有说太多关于它是什么。

作为奖励,async forasync with语法糖?如果是,它们的详细等效形式是什么?

VPf*_*PfB 6

async for并且async with是从低到高发展的逻辑延续。

过去,for编程语言中的循环过去只能简单地迭代线性索引为 0、1、2 ... 最大的值数组。

Python 的for循环是一种更高级的结构。它可以迭代任何支持迭代协议的东西,例如在树中设置元素或节点——它们都没有编号为 0、1、2 等的项目。

迭代协议的核心是__next__特殊方法。每次连续调用都会返回下一项(可能是计算值或检索到的数据)或表示迭代结束。

async for是异步的对手,而不是调用定期__next__它等待异步__anext__和其他一切保持不变。这允许在异步程序中使用常见的习惯用法:

# 1. print lines of text stored in a file
for line in regular_file:
    print(line)

# 2A. print lines of text as they arrive over the network,
#
# The same idiom as above, but the asynchronous character makes
# it possible to execute other tasks while waiting for new data
async for line in tcp_stream:
    print(line)

# 2B: the same with a spawned command
async for line in running_subprocess.stdout:
    print(line)
Run Code Online (Sandbox Code Playgroud)

与的情况async with类似。总而言之:该try .. finally构造被更方便的with块取代- 现在被认为是惯用的 - 可以与支持上下文管理器协议的任何东西进行通信,__enter__以及__exit__进入和退出块的方法。自然地,以前在 atry .. finally中使用的所有内容都被重写为上下文管理器(锁、开闭调用对等)

async with又是异步__aenter____aexit__特殊方法的对应物。当用于进入或退出with块的异步代码等待新数据或锁或某些其他条件被满足时,其他任务可能会运行。

注意:与 不同for,可以使用带有普通(非异步)with语句的异步对象:with await lock:,现在已弃用或不支持。


Mis*_*agi 6

TLDR: forandwith是封装了调用相关方法的几个步骤的重要语法糖。这使得无法这些步骤之间手动添加awaits - 但正确可用/需要它。同时,这意味着获得对他们的支持至关重要。async forwithasync


为什么我们不能做await美好的事情

Python 的语句和表达式由所谓的协议支持:当某个对象用于某些特定的语句/表达式时,Python 会调用该对象上相应的“特殊方法”以允许自定义。例如,x in [1, 2, 3]委托以list.__contains__定义in实际含义。
大多数协议都很简单:每个语句/表达式都会调用一个特殊的方法。如果async我们拥有的唯一特征是原语await,那么我们仍然可以async通过await在正确的位置洒上所有这些“一种特殊方法”语句/表达式“ ” 。

与此相反,forwith陈述都对应于多个步骤:for使用迭代器协议反复获取__next__的迭代器的项目,并with使用该上下文管理器协议进入和退出的上下文。
重要的部分是两者都有多个可能需要异步的步骤。虽然我们可以await在其中一个步骤中手动撒一个,但我们不能全部命中。

  • 更容易查看的情况是with:我们可以分别处理__enter____exit__方法。

    我们可以天真地定义一个带有异步特殊方法的同步上下文管理器。为了进入这实际上是通过await战略性地添加一个:

    with AsyncEnterContext() as acm:
        context = await acm
        print("I entered an async context and all I got was this lousy", context)
    
    Run Code Online (Sandbox Code Playgroud)

    然而,它已经打破了,如果我们使用一个withfor语句情境:我们会先进入所有上下文一次,然后等待它们全部一次

    with AsyncEnterContext() as acm1, AsyncEnterContext() as acm2:
        context1, context2 = await acm1, await acm2  # wrong! acm1 must be entered completely before loading acm2
        print("I entered many async contexts and all I got was a rules lawyer telling me I did it wrong!")
    
    Run Code Online (Sandbox Code Playgroud)

    更糟糕的是,没有一个点可以让我们正确await 退出

虽然for和确实with是语法糖,但它们是不平凡的语法糖:它们使多个动作更好。因此,人们不能天真地对它们进行await 单独的操作。只有一条毯子async withasync for可以覆盖每一步。

为什么我们想要async美好的事物

这两个forwith抽象:他们完全封装的迭代/语境化的想法。

二者择其一,Pythonfor内部迭代的抽象——相比之下,awhile外部迭代的抽象。简而言之,这意味着for程序员不必知道迭代实际上是如何工作的。

  • 比较如何迭代listusing foror while
    some_list = list(range(20))
    index = 0                      # lists are indexed from 0
    while index < len(some_list):  # lists are indexed up to len-1
        print(some_list[index])    # lists are directly index'able
        index += 1                 # lists are evenly spaced
    
    for item in some_list:         # lists are iterable
        print(item)
    
    Run Code Online (Sandbox Code Playgroud) 外部while迭代依赖于关于列表如何具体工作的知识:它可迭代对象中提取实现细节并将它们放入循环中。相比之下,内部for迭代只依赖于知道列表是可迭代的。它适用于任何列表的实现,实际上任何可迭代对象的实现。

底线是for——而且with——不要打扰实施细节。这包括需要知道哪些我们需要异步洒步骤。只有一条毯子async withasync for可以在我们不知道是哪一步的情况下覆盖每一步。

为什么我们需要async美好的事物

一个有效的问题是为什么forwith获取async变体,但其他人没有。有一个关于一个微妙的点forwith是不是在日常使用很明显:既代表并发-而并发的域async

无需过多赘述,简单的解释是处理例程 ( ())、可迭代对象 ( for) 和上下文管理器 ( with)的等效性。正如问题中引用答案中所确定的那样,协程实际上是一种生成器。显然,生成器也是可迭代的,事实上我们可以通过生成器表达任何可迭代的。不太明显的部分是上下文管理器也相当于生成器——最重要的是,contextlib.contextmanager可以将生成器转换为上下文管理器。

为了始终如一地处理各种并发,我们需要async例程 ( await)、可迭代对象 ( async for) 和上下文管理器 ( async with) 的变体。只需一条毯子async withasync for就能始终如一地覆盖每一步。


Cha*_*ker 4

我的理解async with是,它允许 python 调用await上下文管理器中的关键字,而不会让 python 惊慌失措。async从中删除with错误。这很有用,因为创建的对象很可能会执行我们必须等待的昂贵的 io 操作 - 因此我们可能会等待从这个特殊的异步上下文管理器创建的对象的方法。如果没有正确地关闭和打开上下文管理器,可能会在 python 中产生问题(否则为什么要让 python 用户学习更细致的语法和语义呢?)。

\n

我还没有完全测试async for它的作用或复杂性,但很想看到一个示例,并且稍后可能会在需要时对其进行测试并更新此答案。一旦我到达它,我会将示例放在这里:https ://github.com/brando90/ultimate-utils/blob/master/tutorials_for_myself/concurrency/asyncio_for.py

\n

现在请参阅我带注释的示例async with(脚本位于https://github.com/brando90/ultimate-utils/blob/master/tutorials_for_myself/concurrency/asyncio_my_example.py):

\n
"""\n1. https://realpython.com/async-io-python/#the-asyncawait-syntax-and-native-coroutines\n2. https://realpython.com/python-concurrency/\n3. /sf/ask/4696444931/\n\ntodo - async with, async for.\n\ntodo: meaning of:\n    - The async for and async with statements are only needed to the extent that using plain for or with would \xe2\x80\x9cbreak\xe2\x80\x9d\n        the nature of await in the coroutine. This distinction between asynchronicity and concurrency is a key one to grasp\n    - One exception to this that you\xe2\x80\x99ll see in the next code is the async with statement, which creates a context\n        manager from an object you would normally await. While the semantics are a little different, the idea is the\n        same: to flag this context manager as something that can get swapped out.\n    - download_site() at the top is almost identical to the threading version with the exception of the async keyword on\n        the function definition line and the async with keywords when you actually call session.get().\n        You\xe2\x80\x99ll see later why Session can be passed in here rather than using thread-local storage.\n    - An asynchronous context manager is a context manager that is able to suspend execution in its enter and exit\n        methods.\n"""\n\nimport asyncio\nfrom asyncio import Task\n\nimport time\n\nimport aiohttp\nfrom aiohttp.client_reqrep import ClientResponse\n\nfrom typing import Coroutine\n\n\nasync def download_site(coroutine_name: str, session: aiohttp.ClientSession, url: str) -> ClientResponse:\n    """\n    Calls an expensive io (get data from a url) using the special session (awaitable) object. Note that not all objects\n    are awaitable.\n    """\n    # - the with statement is bad here in my opion since async with is already mysterious and it\'s being used twice\n    # async with session.get(url) as response:\n    #     print("Read {0} from {1}".format(response.content_length, url))\n    # - this won\'t work since it only creates the coroutine. It **has** to be awaited. The trick to have it be (buggy)\n    # synchronous is to have the main coroutine call each task we want in order instead of giving all the tasks we want\n    # at once to the vent loop e.g. with the asyncio.gather which gives all coroutines, gets the result in a list and\n    # thus doesn\'t block!\n    # response = session.get(url)\n    # - right way to do async code is to have this await so someone else can run. Note, if the download_site/ parent\n    # program is awaited in a for loop this won\'t work regardless.\n    response = await session.get(url)\n    print(f"Read {response.content_length} from {url} using {coroutine_name=}")\n    return response\n\nasync def download_all_sites_not_actually_async_buggy(sites: list[str]) -> list[ClientResponse]:\n    """\n    Code to demo the none async code. The code isn\'t truly asynchronous/concurrent because we are awaiting all the io\n    calls (to the network) in the for loop. To avoid this issue, give the list of coroutines to a function that actually\n    dispatches the io like asyncio.gather.\n\n    My understanding is that async with allows the object given to be a awaitable object. This means that the object\n    created is an object that does io calls so it might block so it\'s often the case we await it. Recall that when we\n    run await f() f is either 1) coroutine that gains control (but might block code!) or 2) io call that takes a long\n    time. But because of how python works after the await finishes the program expects the response to "actually be\n    there". Thus, doing await blindly doesn\'t speed up the code. Do awaits on real io calls and call them with things\n    that give it to the event loop (e.g. asyncio.gather).\n\n    """\n    # - create a awaitable object without having the context manager explode if it gives up execution.\n    # - crucially, the session is an aiosession - so it is actually awaitable so we can actually give it to\n    # - asyncio.gather and thus in the async code we truly take advantage of the concurrency of asynchronous programming\n    async with aiohttp.ClientSession() as session:\n    # with aiohttp.ClientSession() as session:  # won\'t work because there is an await inside this with\n        tasks: list[Task] = []\n        responses: list[ClientResponse] = []\n        for i, url in enumerate(sites):\n            task: Task = asyncio.ensure_future(download_site(f\'coroutine{i}\', session, url))\n            tasks.append(task)\n            response: ClientResponse = await session.get(url)\n            responses.append(response)\n        return responses\n\n\nasync def download_all_sites_truly_async(sites: list[str]) -> list[ClientResponse]:\n    """\n    Truly async program that calls creates a bunch of coroutines that download data from urls and the uses gather to\n    have the event loop run it asynchronously (and thus efficiently). Note there is only one process though.\n    """\n    # - indicates that session is an async obj that will likely be awaited since it likely does an expensive io that\n    # - waits so it wants to give control back to the event loop or other coroutines so they can do stuff while the\n    # - io happens\n    async with aiohttp.ClientSession() as session:\n        tasks: list[Task] = []\n        for i, url in enumerate(sites):\n            task: Task = asyncio.ensure_future(download_site(f\'coroutine{i}\', session, url))\n            tasks.append(task)\n        responses: list[ClientResponse] = await asyncio.gather(*tasks, return_exceptions=True)\n        return responses\n\n\nif __name__ == "__main__":\n    # - args\n    sites = ["https://www.jython.org", "http://olympus.realpython.org/dice"] * 80\n    start_time = time.time()\n\n    # - run main async code\n    # main_coroutine: Coroutine = download_all_sites_truly_async(sites)\n    main_coroutine: Coroutine = download_all_sites_not_actually_async_buggy(sites)\n    responses: list[ClientResponse] = asyncio.run(main_coroutine)\n\n    # - print stats\n    duration = time.time() - start_time\n    print(f"Downloaded {len(sites)} sites in {duration} seconds")\n    print(\'Success, done!\\a\')\n
Run Code Online (Sandbox Code Playgroud)\n