[python]前往python async -2。发电机作为迭代器的旅程
#网络开发人员 #python #网络

为什么要发电机?

正如先前宣布的那样,我们的旅程始于关于Python发电机的讨论。但为什么?要概括,让我们写一个非常简单的async函数并查看其执行结果。

async def journey() -> None:
  return 1

ret = journey()
print(type(ret)) 
<class 'coroutine'>
sys:1: RuntimeWarning: coroutine 'journey' was never awaited

请忽略警告。如果它是一个简单的同步函数(即只是正常的python函数),那将是<class ‘int’>,因为该函数返回1。但是在这种情况下,我们还有另一个称为coroutine的python对象。但是什么是Coroutine?要获取一些提示,让我们打开模块typing并查找typing.Coroutine

# some parts are omitted
# …
class Awaitable(Protocol[_T_co]):
    @abstractmethod
    def __await__(self) -> Generator[Any, None, _T_co]: ...

class Coroutine(Awaitable[_V_co], Generic[_T_co, _T_contra, _V_co]):
# …

因此,我们可以看到Python中的Coroutine对象与发电机密切相关。实际上,在整个Python历史中,它已经从发电机对象的概念中演变出来。因此,我要说的是要了解发电机以研究Python中的Coroutine对象。

这是一个相当长的介绍。让我们从python发电机开始。

那么什么是发电机?

我假设您经常遇到过关键字的收益或屈服,或者至少在您超出了Python的初学者或任何其他编程语言的范围之内。

至少在Python的情况下,此yield关键字始终与“发电机”一词。它们实际上是define each other-生成器必须具有关键字yield,并且带有yield的函数被识别为生成器函数(使生成器可以执行其主体的发电机)。

但是,在我们研究这个yield的作用之前,让我们考虑一下“发电机”一词的含义。 yield与“发电机”一词有什么关系?如果我们回到docs about the yield expression的解释),则说

调用发电机函数时,它将返回迭代器称为生成器。

对。发电机是迭代器。但是,什么是python中的迭代器?

迭代器

简而言之,迭代器是一个对访问集合对象中元素(例如列表,字典或python中设置的元素)提供一致接口的对象(因此,它不仅应用于Python,而且是通用的概念:咨询G.o.F book中的迭代章节。在Python中,具体来说,具有__next__() dunder的任何类都可以是迭代器,并且当我们使用for … in …循环穿越对象时,将执行此__next__()方法,从而从我们的兴趣集合中给出了当前元素的下一个元素。

备注:有些人可能对__iter__() dunder感到好奇。实际上,这就是使对象iTera*ble*的原因,而不是itera*ter*。如果我从g.o.f借用这些单词,那么一个疑问是创建迭代器的工厂对象。但是,挖掘这一点与我们当前的上下文相比,所以我想在这里停下来。对于那些有兴趣比较Python中的迭代和迭代器的人,有很多资源,例如RealPython的资源。

因此,迭代器仅关注跟踪下一个对象。而且,这是生成器遵循的确切接口。

发电机作为迭代器

因此,生成器是迭代器。这意味着,通过考虑“下一步”的内容,发电机会关心向用户提供的数据集合。但是如何?

许多材料在懒惰加载的上下文中引入生成器:它仅在必要时才检索数据。为了将此功能连接到我们先前关于迭代器的简短讨论,只有在需要时,生成器才会检索 - 接下来的数据在发电机试图获取之前不存在内存中。

作为一个例子,您将从以下代码中清楚地看到此懒惰加载功能:

from typing import TypeVar


T = TypeVar("T")


def get_element(*, element: T) -> T:
  print(f"element generated: {element}")
  return element


if __name__ == "__main__":
  collection = ['hitotsu', 2, "three", 4, "go"]

  print("--- non-generator(list comprehension) ---")
  non_generator = [get_element(element=element) for element in collection]

  for element in non_generator:
    print(f"print element: {element}")

  print("--- non-generator test ends ---")

  print("--- generator(generator expression) ---")
  generator = (get_element(element=element) for element in collection)

  for element in generator:
    print(f"print element: {element}")

  print("--- generator test ends ---")

结果应为:

--- non-generator(list comprehension) ---
element generated: hitotsu
element generated: 2
element generated: three
element generated: 4
element generated: go
print element: hitotsu
print element: 2
print element: three
print element: 4
print element: go
--- non-generator test ends ---
--- generator(generator expression) ---
element generated: hitotsu
print element: hitotsu
element generated: 2
print element: 2
element generated: three
print element: three
element generated: 4
print element: 4
element generated: go
print element: go
--- generator test ends ---

,简而言之,发电机是按需返回下一个元素的迭代器。

yield在发电机中的作用

备注:Python文档的解释有些简洁:了解yield和Coroutine的概念,请参阅此priceless Youtube video by ByteByteGo

但是,这似乎并不是使用发电机而不是普通迭代器的特殊好处。如果我们要谈论每个元素的大量计算(这通常是我们应用懒惰加载概念的原因),那么我们可以通过使用普通迭代器而不是直接预先计算下一个元素来解决方法,以实现懒惰的加载。

所以而不是准备

data = [heavy1, heavy2, ..., heavy10]

我们只能运行

for i in range(10): heavy = heavy_computation

但是,为什么我们仍然认为发电机是有价值的?请注意,我们还没有讨论过有关yield!。

说我们要产生一个数据集,其中每个元素都是某种重型计算过程的结果。如果我们只使用像range这样的普通迭代器,则可以编写这样的代码:

class HeavyComputationResult:
  # this is just for type annotation!
  

def heavy_computation(*, arg: int) -> HeavyComputationResult:
   local_heavy_var = 

  # …
  return something

for i in range(5):
  something = heavy_computation(arg=i)
  # do something else

这意味着,对于每一个迭代,我们都需要调用此heavy_computation功能,这可能需要大量的堆栈内存。这可能是计算负担,因为CPU不仅需要操作堆栈内存,还需要执行其他CPU密集型任务,例如计算本地变量,这些变量可能不会在整个呼叫堆栈中发生变化。

这就是我们的yield作为解决方案的位置。如果您阅读了docs或我在此yield表达式上介绍的任何其他材料,则它保留了呼叫箱,并且只能产生控制流,因此我们不必冗余地计算局部变量。

因此我们的代码可以如下改进:

def heavy_computation_generator(*, iter_times: int) -> HeavyComputationResult:
  local_heavy_var = 

  for i in range(iter_times):
    yield something

so local_heavy_var仅在此 Generator函数中仅被称为一次,现在我们可以一起保存我们的内存和时间。

简单(=经典)Coroutine

到目前为止,我们的发电机只会产生(=生成)数据,但是毕竟这与异步API有何关系?您会记得我们关于发电机的讨论最初来自Coroutine。但是,如果您在Google上搜索“ Coroutine Python”一词,那么那里的大多数材料都会带有asyncawait等关键字,这显然意味着您会阅读有关异步API的阅读。那么发电机和python中的coroutines之间的差距在哪里?

从PEP 342中,您可以在此差距上发现一些线索。 Motivation部分清楚地说

  • 发电机确实具有暂停功能(使用yield),但它仅产生输出数据,无法消耗 输入数据
  • 因此,有很多限制,但是主要的局限性之一是程序员无法完全控制逻辑流,因为生成器没有侦听(=获取输入)到程序员< /li>
  • 一个含义是发电机无法彼此交流。即,在交换控制流程时保持堆栈帧相当难以实现,因为我们无法直接操纵运行的生成器

因此,正如PEP 342所指出的那样,Python中的Coroutine概念是基于发电机发明的。这个过渡概念称为“简单的Coroutine”或“经典Coroutine”。在这里,我们想将其称为简单,之后Pep342。要充分了解本机的Coroutine在Python中的工作方式,我们需要先研究这个简单的Coroutine,这将在此中进行讨论。下一篇文章。

结论

在这篇文章中,我们讨论了什么是生成器:它是一个按需检索数据的迭代器对象,并保持其堆栈框架以进行更高的性能。

请留在下一篇文章中,作为Coroutines。