Python中的生成器是如何工作的

11 投票
1 回答
2314 浏览
提问于 2025-04-18 16:45

我刚开始学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内部创建了什么?我知道yield会创建一个生成器对象,但我想知道生成器对象内部包含了什么,使它们能够工作?它们只是一个状态的堆栈/列表吗?我们使用.next方法提取每个状态,Python会自动调用带有索引状态的函数吗?

1 个回答

21

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

这句话是对的。包含yield的函数返回的就是一个生成器对象。生成器对象是一种迭代器,每次迭代时会返回代码中通过yield语句产生的值。

生成器对象就像一个包含状态的栈。

生成器对象里有一个指针,指向当前执行的状态,还有很多其他的东西用来保持生成器的状态。执行状态就像是生成器代码的调用栈。

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

差不多是这样的。当你调用next(gen_object)时,Python会评估当前的执行状态:

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是用来解释Python字节码的最高级函数:

PyObject PyEval_EvalFrameEx(PyFrameObject f, int throwflag)

这是Python解释的主要函数,代码有2000行那么多。与执行状态f相关的代码对象会被执行,解释字节码并根据需要执行调用。额外的throwflag参数大多数情况下可以忽略——如果为真,它会立即抛出一个异常;这个参数用于生成器对象的throw()方法。

当它在评估字节码时遇到yield,就会把正在yield的值返回给调用者:

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

当你使用yield时,当前执行状态的值会被保留(通过f->f_stacktop = stack_pointer),这样当再次调用next时,就能从上次停止的地方继续。所有非生成器函数在完成评估后会把f_stacktop设置为NULL。所以当你再次调用生成器对象的next时,PyEval_ExvalFrameEx会再次被调用,使用之前的状态指针。这个指针的状态和上次yield时完全一样,因此执行会从那个点继续。实际上,当前的执行状态是“冻结”的。这在引入生成器的PEP中有描述:

如果遇到一个yield语句,函数的状态就会被冻结,返回的值会传递给.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等可能会以完全不同的方式实现生成器。我鼓励你去阅读生成器对象的源代码,以了解更多关于CPython实现的内容。

撰写回答