生成器函数是Python编程语言最酷的功能之一。 网络上有许多文章描述了生成器函数在我们的python程序的速度,可伸缩性和内存效率方面提供的许多好处。 但是,那里没有太多资料可以说明发电机功能在幕后的实际工作方式。 本文试图通过阐明python编程语言的一些关键功能来填补这一空白,这些功能使生成器功能成为可能。
赋予发电机功能超能力的基本特征是发电机功能可以暂停然后恢复的能力 在任何时候任何功能。 暂停函数后,生成器函数的本地状态保持不变,并且在再次恢复该函数时可用。 那怎么可能? 如何暂停并在保持其本地状态不变的情况下恢复功能? 就我们所知,函数只有一个入口点 和 多个出口 ( 返回 声明)。 每次我们调用一个函数时,代码都会从该函数的第一行开始执行,直到遇到退出点为止。 此时,控制权将返回给函数的调用者,并且该函数的局部变量堆栈将被清除,并且操作系统将回收相关的内存。
生成器函数却不会以这种方式运行。 他们有多个 入口和出口点。 生成器函数中的每个yield语句同时定义一个退出点和一个重新进入点。 生成器功能的执行将继续,直到屈服为止 遇到语句。 此时,将保留函数的本地状态,并将控制流交给生成器函数的调用者。 恢复生成器功能(通过调用next , send或通过for循环迭代)后,它将恢复其最后一个已知的本地状态,并从上一次暂停生成器功能的yield语句之后的行开始执行。 这种行为令人难以置信,不符合功能正常行为。
为了尝试理解生成器函数背后的魔力,让我们从仔细看一下正常函数开始:
每次调用add_two_numbers时 ,我们期望CPython解释器创建一个新的堆栈框架对象并在该对象的上下文中执行add_two_numbers函数。 我们期望局部变量s被压入此堆栈框架并保持在那里,直到函数退出。 在函数退出时,我们希望清除关联的堆栈框架并回收相应的内存。 让我们确认是这种情况:
我们使用内置检查 模块以捕获add_two_numbers函数的当前执行帧。 最后,我们打印堆栈框架对象以及与之关联的任何局部变量。 我们希望堆栈框架为空,因此没有局部变量。 让我们继续执行上面的代码片段:
什么?! 在对add_two_numbers的调用结束之后,堆栈框架及其所有关联的局部变量仍然悬而未决 ! 这是怎么回事 我们是否偶然发现了CPython中的内存泄漏? 不,不是这样。 这种观察将我们带到了Python堆栈框架的基本特征之一: Python堆栈框架未分配在堆栈内存中。 相反,它们分配在堆内存上 。 从本质上讲,这意味着python堆栈框架可以超过其各自的函数调用! 生成器函数利用此行为来发挥作用。
当CPython编译器在函数中遇到yield语句时,它将在编译后的代码对象上设置一个标志,以告知CPython解释器该函数是生成器函数。 我们可以使用dis模块来查看实际效果:
当CPython解释器在与函数关联的代码对象上看到GENERATOR标志时,它不执行该函数,而是返回一个生成器对象。 生成器对象是一个迭代器。 这意味着我们可以使用next关键字或for循环遍历它。
当我们迭代生成器函数时,执行将继续直到遇到yield语句。 此时,函数的堆栈框架被冻结,控制权返回给生成器函数的调用者。
当我们通过调用next或通过for循环继续前进生成器函数时,执行恰恰从上次中断的点(最后一个yield语句)开始。 CPython解释器如何知道上一次停止执行生成器函数的实例的位置? 它通过与正在执行的生成器实例相关联的堆栈框架对象知道这一点。
前面我们已经看到,Python堆栈帧是在堆内存上分配的,它们的状态在后续对next的调用之间得以保留。 或发送 在生成器函数的实例上(当然,生成器函数的每个新实例都会获得一个新的堆栈框架)。 除了存储有关局部变量和全局变量的信息外,python堆栈框架还封装了其他有用的信息位。 一个这样的一条信息是最后一个 指令 指针 。
最后一条指令指针 是指向与生成器函数的主体相关联的代码对象的字节码字符串的索引,并指向在堆栈帧的上下文中运行的最后一个字节码指令。 恢复生成器函数的实例时,CPython解释器将使用最后一条指令指针 上 的 相关的堆栈框架,确定从哪里开始执行生成函数的代码对象。 我们可以使用dis模块提供的便捷迪斯科方法以交互方式看到此情况 :
我们创建了一个生成两个数字的简单生成器函数。 调用generator函数将创建并返回一个generator对象。 在此调用期间,生成器函数主体中没有代码被执行,并且最后一条指令指针被初始化为-1。 当我们通过调用next来执行生成器函数时, 最后一条指令指针从一个yield语句前进到另一个(在每次调用之后, 最后一条指令指针的当前位置,在上述代码段中用→表示),暂停然后从直到生成器函数用尽并引发StopIteration异常为止。
总结起来,关键要记住的是python生成器封装了堆栈框架和代码对象。 堆栈帧分配在堆内存上,并拥有指向堆栈帧上下文中在代码对象上运行的最后一个字节码指令的指针。 它是最后一个指令指针,它告诉CPython解释器在恢复生成器函数时接下来要执行哪一行。 这些是发电机功能蓬勃发展的核心组成部分。
如果您喜欢冒险,可以使用_PyEval_EvalCodeWithName函数 在Python / ceval.c和Python / genobject.c中 CPython源代码中的模块以查看实现细节。 该博客使用CPython 3.6的源代码编写。
如果生成器功能对您来说是一个谜,希望这篇文章有助于使您更清楚地了解生成器功能的工作方式。
From: https://hackernoon.com/the-magic-behind-python-generator-functions-bc8eeea54220