为什么我们需要`asyncfor`和`asyncwith`?
引入async for和的意义async with何在?我知道这些陈述有 PEP,但它们显然是为语言设计者准备的,而不是像我这样的普通用户。将不胜感激补充示例的高级理由。
我自己做了一些研究并找到了这个答案:
该
async for和async with语句必要的,因为你会打破yield from/await与裸链for和with报表。
作者没有举例说明链条是如何断裂的,所以我仍然很困惑。此外,我注意到 Python 有async forand async with,但没有async whileand async try ... except。这听起来很奇怪,因为和分别是for和 的with语法糖。我的意思是,考虑到后者是前者的构建块,它们的版本不会允许更大的灵活性吗?whiletry ... exceptasync
还有另一种答案讨论async for,但它仅覆盖它是什么并不对,并没有说太多关于它是什么。
作为奖励,async for和async with语法糖?如果是,它们的详细等效形式是什么?
回答
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)
与的情况async with类似。总而言之:该try .. finally构造被更方便的with块取代- 现在被认为是惯用的 - 可以与支持上下文管理器协议的任何东西进行通信,__enter__以及__exit__进入和退出块的方法。自然地,以前在 atry .. finally中使用的所有内容都被重写为上下文管理器(锁、开闭调用对等)
async with又是异步__aenter__和__aexit__特殊方法的对应物。当用于进入或退出with块的异步代码等待新数据或锁或某些其他条件被满足时,其他任务可能会运行。
注意:与 不同for,可以使用带有普通(非异步)with语句的异步对象:with await lock:,现在已弃用或不支持。
回答
TLDR: forandwith是封装了调用相关方法的几个步骤的重要语法糖。这使得无法在这些步骤之间手动添加awaits - 但正确可用/需要它。同时,这意味着获得对他们的支持至关重要。async forwithasync
为什么我们不能做await美好的事情
Python 的语句和表达式由所谓的协议支持:当某个对象用于某些特定的语句/表达式时,Python 会调用该对象上相应的“特殊方法”以允许自定义。例如,x in [1, 2, 3]委托以list.__contains__定义in实际含义。
大多数协议都很简单:每个语句/表达式都会调用一个特殊的方法。如果async我们拥有的唯一特征是原语await,那么我们仍然可以async通过await在正确的位置洒上所有这些“一种特殊方法”语句/表达式“ ” 。
与此相反,for和with陈述都对应于多个步骤: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)然而,它已经打破了,如果我们使用一个
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!")更糟糕的是,没有一个点可以让我们正确
await退出。
虽然for和确实with是语法糖,但它们是不平凡的语法糖:它们使多个动作更好。因此,人们不能天真地对它们进行await 单独的操作。只有一条毯子async with,async for可以覆盖每一步。
为什么我们想要async美好的事物
这两个for和with是抽象:他们完全封装的迭代/语境化的想法。
二者择其一,Pythonfor是内部迭代的抽象——相比之下,awhile是外部迭代的抽象。简而言之,这意味着for程序员不必知道迭代实际上是如何工作的。
- 比较如何迭代
listusingfororwhile: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)外部
while迭代依赖于关于列表如何具体工作的知识:它从可迭代对象中提取实现细节并将它们放入循环中。相比之下,内部for迭代只依赖于知道列表是可迭代的。它适用于任何列表的实现,实际上任何可迭代对象的实现。
底线是for——而且with——不要打扰实施细节。这包括需要知道哪些我们需要异步洒步骤。只有一条毯子async with,async for可以在我们不知道是哪一步的情况下覆盖每一步。
为什么我们需要async美好的事物
一个有效的问题是为什么for和with获取async变体,但其他人没有。有一个关于一个微妙的点for和with是不是在日常使用很明显:既代表并发-而并发的域async。
无需过多赘述,简单的解释是处理例程 ( ())、可迭代对象 ( for) 和上下文管理器 ( with)的等效性。正如问题中引用的答案中所确定的那样,协程实际上是一种生成器。显然,生成器也是可迭代的,事实上我们可以通过生成器表达任何可迭代的。不太明显的部分是上下文管理器也相当于生成器——最重要的是,contextlib.contextmanager可以将生成器转换为上下文管理器。
为了始终如一地处理各种并发,我们需要async例程 ( await)、可迭代对象 ( async for) 和上下文管理器 ( async with) 的变体。只需一条毯子async with,async for就能始终如一地覆盖每一步。