【问题标题】:how generators work in python生成器如何在 python 中工作
【发布时间】:2014-10-03 15:16:33
【问题描述】:

我是 Python 和编程的新手。对于新程序员来说,生成器有点太复杂了。这是我关于 Python 中生成器函数的理论:

  1. 任何包含yield 语句的函数都会返回一个生成器对象

  2. 生成器对象是包含状态的堆栈

  3. 每次我调用 .next 方法时,Python 都会提取函数的状态,当它找到另一个 yield 语句时,它会再次绑定状态并删除之前的状态:

例子:

 [ 
  [state1] # Stack contains states and states contain info about the function
  [state2] # State1 will be deleted when python finds the other yield? 
 ] 

这当然可能是世界上最愚蠢的理论,但请原谅我只是编码方面的新手。

我的问题:

  1. Python 内部使用什么来存储状态?

  2. yield 语句是否将状态添加到堆栈(如果存在)?

  3. 内部产生什么产量?我知道 yield 创建了一个生成器对象,但是,我想知道生成器对象包含哪些使它们工作的内容?它们只是状态的堆栈/列表,我们使用.next 方法来提取每个状态,Python 会自动调用具有索引状态的函数吗?

【问题讨论】:

  • 由于这个问题似乎与内部有关,如果您想了解生成器是什么以及如何在用户级别使用它们,请参阅this question
  • @georg IMO 您标记为欺骗的问题没有回答 OP 的问题。 OP 正在询问生成器的内部结构,这个问题实际上并没有涵盖。实际上,我认为您在 cmets 中提供的另一个问题更接近准确的欺骗(尤其是 jsbueno 的回答)。
  • 生成器的实现可能因实现而异;特定实现的源代码将是一个很好的起点。事实上,这个问题相当广泛。

标签: python generator yield-keyword


【解决方案1】:

任何包含yield语句的函数都会返回一个生成器对象

这是正确的。包含yield 的函数的返回值是一个生成器对象。生成器对象是一个迭代器,其中每次迭代都会返回一个值,该值是从支持生成器的代码中yielded。

生成器对象是包含状态的堆栈

生成器对象包含一个指向当前执行帧的指针,以及用于维护生成器状态的一大堆其他内容。执行帧包含生成器中代码的调用堆栈。

每次我调用 .next 方法时,Python 都会提取函数的状态并 当它找到另一个 yield 语句时,它将再次绑定状态并 删除之前的状态

有点。当你调用next(gen_object),Python evaluates the current execution frame

gen_send_ex(PyGenObject *gen, PyObject *arg, int exc) {  // This is called when you call next(gen_object)
    PyFrameObject *f = gen->gi_frame;
    ...
    gen->gi_running = 1;
    result = PyEval_EvalFrameEx(f, exc);  // This evaluates the current frame
    gen->gi_running = 0; 

PyEval_EvalFrame 是最高级别的函数used to interpret Python bytecode

PyObject* PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)

这是 Python 解释的主要、朴素的功能。它是 字面意思是 2000 行长。与关联的代码对象 执行帧 f 被执行,解释字节码并执行 根据需要调用。附加的 throwflag 参数大多可以是 忽略 - 如果为真,则立即引发异常 抛出; this 用于生成器对象的 throw() 方法。

它知道当它在评估字节码时遇到yield,它应该return the value being yielded to the caller

    TARGET(YIELD_VALUE) {
        retval = POP();
        f->f_stacktop = stack_pointer;
        why = WHY_YIELD;
        goto fast_yield;
    }

当你让出时,帧的值堆栈的当前值被保持(通过f->f_stacktop = stack_pointer),这样我们可以在再次调用next 时从中断的地方继续。所有非生成器函数在完成评估后将f_stacktop 设置为NULL。因此,当您在生成器对象上再次调用next 时,会再次调用PyEval_ExvalFrameEx,使用与以前相同的帧指针。指针的状态将与前一次产生时完全相同,因此将从该点继续执行。本质上,框架的当前状态是“冻结的”。这在PEP that introduced generators 中有描述:

如果遇到yield语句,函数的状态是 冻结,并将值 [yielded] 返回到 .next() 的 呼叫者。 “冻结”是指保留所有本地状态, 包括当前的局部变量绑定,指令 指针和内部评估堆栈:足够的信息是 保存以便下次调用 .next() 时,该函数可以 就像 yield 语句只是另一个外部语句一样进行 打电话。

这是生成器对象维护的大部分状态(直接取自其头文件):

typedef struct {
    PyObject_HEAD
    /* The gi_ prefix is intended to remind of generator-iterator. */

    /* Note: gi_frame can be NULL if the generator is "finished" */
    struct _frame *gi_frame;

    /* True if generator is being executed. */
    char gi_running;

    /* The code object backing the generator */
    PyObject *gi_code;

    /* List of weak reference. */
    PyObject *gi_weakreflist;

    /* Name of the generator. */
    PyObject *gi_name;

    /* Qualified name of the generator. */
    PyObject *gi_qualname;
} PyGenObject;

gi_frame是指向当前执行帧的指针。

请注意,所有这些都是特定于 CPython 实现的。 PyPy/Jython/等。很可能以完全不同的方式实现生成器。我鼓励您read through the source for generator objects 了解更多关于 CPython 的实现。

【讨论】:

  • 这是一个非常有趣的答案。我很想知道你经历了怎样的过程才能得到如此详细的答案。您可以建议任何用于源代码检查的工具/资源/方法?
  • @cglacet 我相信我只是花了几个小时阅读 Python 源代码、文档和 PEP。我不记得我是在本地查看源代码并使用 vim 浏览它,还是只是使用旧 Mercurial repo 的 web-viewer。我认为是网络查看器。不过,一个好的 IDE 可能会更容易跳过源代码。我真的没有什么特别的技巧,我只是搜索代码,直到找到与生成器相关的东西,然后阅读/重新阅读,并查阅文档,直到我能理解它在做什么。
猜你喜欢
  • 2016-10-05
  • 2015-02-12
  • 2017-01-17
  • 2019-07-10
  • 2013-07-24
  • 2018-12-18
  • 2021-11-19
  • 1970-01-01
  • 2020-07-08
相关资源
最近更新 更多