Python C代码中的yield如何工作,优缺点是什么

6 投票
1 回答
1672 浏览
提问于 2025-04-18 11:40

最近我在研究Python的代码。我知道怎么使用生成器(比如next、send等等),但通过阅读Python的C代码来理解它的工作原理真的很有趣。

我在Object/genobject.c找到了相关代码,虽然理解起来不算太难,但也不是特别简单。所以我想搞清楚它到底是怎么工作的,确保我对Python中的生成器没有误解。

我知道所有的调用都是

static PyObject *
gen_send_ex(PyGenObject *gen, PyObject *arg, int exc)

结果是从PyEval_EvalFrameEx返回的,这看起来像是一个动态的框架结构,我能把它理解为stack(栈)之类的吗?

好的,看起来Python在内存中存储了一些上下文(我理解得对吗?)。每次我们使用yield时,它都会创建一个生成器,并在内存中存储上下文,尽管并不是所有的函数和变量都会被存储。

我知道如果我有一个大的循环或者需要解析的大数据,使用yield是非常棒的,它节省了很多内存,并且让事情变得简单。但我的一些同事喜欢到处使用yield,就像使用return一样。这让代码不太容易阅读和理解,而且Python会为大多数可能永远不会再被调用的函数存储上下文。这算不算一种不好的做法?

所以,我的问题是:

  1. PyEval_EvalFrameEx是怎么工作的。
  2. yield的内存使用情况。
  3. 到处使用yield算不算不好的做法。

我发现如果我有一个生成器,函数gen_send_ex会被调用两次,为什么呢?

def test():
    while 1:
        yield 'test here'

test().next()

它会第一次调用gen_send_ex时不带参数,第二次带参数,然后得到结果。

谢谢你的耐心。

1 个回答

1

我看到了一些文章:

这篇文章告诉我 PyEval_EvalFrameEx 是怎么工作的。

http://tech.blog.aknin.name/2010/09/02/pythons-innards-hello-ceval-c-2/

这篇文章告诉我 Python 中的 frame 结构。

http://tech.blog.aknin.name/2010/07/22/pythons-innards-interpreter-stacks/

这两样东西对我们来说非常重要。

所以让我自己来回答我的问题。我不知道我是否正确。

如果我有误解或者完全错了,请告诉我。

如果我有代码:

def gen():                                                                                                                                                                
    count = 0                                                                           
    while count < 10:                                                                   
        count += 1                                                                      
        print 'call here'                                                               
        yield count

这就是一个非常简单的生成器。

f = gen()

每次我们调用它时,Python 会创建一个生成器对象。

PyObject *                                                                           
PyGen_New(PyFrameObject *f)                                                          
{                                                                                    
    PyGenObject *gen = PyObject_GC_New(PyGenObject, &PyGen_Type);                    
    if (gen == NULL) {                                                               
        Py_DECREF(f);                                                                
        return NULL;                                                                 
    }                                                                                
    gen->gi_frame = f;                                                               
    Py_INCREF(f->f_code);                                                            
    gen->gi_code = (PyObject *)(f->f_code);                                          
    gen->gi_running = 0;                                                             
    gen->gi_weakreflist = NULL;                                                      
    _PyObject_GC_TRACK(gen);                                                         
    return (PyObject *)gen;                                                          
}

我们可以看到它初始化了一个生成器对象,并且初始化了一个 Frame

我们做的任何事情,比如 f.send()f.next(),都会调用 gen_send_ex,以及下面的代码:

static PyObject *                                                                    
gen_iternext(PyGenObject *gen)                                                                                                                                           
{                                                                                    
    return gen_send_ex(gen, NULL, 0);                                                
}

static PyObject *                                                                    
gen_send(PyGenObject *gen, PyObject *arg)                                            
{                                                                                    
    return gen_send_ex(gen, arg, 0);                                                 
}

这两个函数之间唯一的区别是参数,send 是发送一个参数,next 是发送 NULL。

gen_send_ex 的代码如下:

static PyObject *
gen_send_ex(PyGenObject *gen, PyObject *arg, int exc)
{
    PyThreadState *tstate = PyThreadState_GET();
    PyFrameObject *f = gen->gi_frame;
    PyObject *result;

    if (gen->gi_running) {
        fprintf(stderr, "gi init\n");
        PyErr_SetString(PyExc_ValueError,
                        "generator already executing");
        return NULL;
    }
    if (f==NULL || f->f_stacktop == NULL) {
        fprintf(stderr, "check stack\n");
        /* Only set exception if called from send() */
        if (arg && !exc)
            PyErr_SetNone(PyExc_StopIteration);
        return NULL;
    }

    if (f->f_lasti == -1) {
        fprintf(stderr, "f->f_lasti\n");
        if (arg && arg != Py_None) {
            fprintf(stderr, "something here\n");
            PyErr_SetString(PyExc_TypeError,
                            "can't send non-None value to a "
                            "just-started generator");
            return NULL;
        }
    } else {
        /* Push arg onto the frame's value stack */
        fprintf(stderr, "frame\n");
        if(arg) {
            /* fprintf arg */
        }
        result = arg ? arg : Py_None;
        Py_INCREF(result);
        *(f->f_stacktop++) = result;
    }

    fprintf(stderr, "here\n");
    /* Generators always return to their most recent caller, not
     * necessarily their creator. */
    Py_XINCREF(tstate->frame);
    assert(f->f_back == NULL);
    f->f_back = tstate->frame;

    gen->gi_running = 1;
    result = PyEval_EvalFrameEx(f, exc);
    gen->gi_running = 0;

    /* Don't keep the reference to f_back any longer than necessary.  It
     * may keep a chain of frames alive or it could create a reference
     * cycle. */
    assert(f->f_back == tstate->frame);
    Py_CLEAR(f->f_back);

    /* If the generator just returned (as opposed to yielding), signal
     * that the generator is exhausted. */
    if (result == Py_None && f->f_stacktop == NULL) {
        fprintf(stderr, "here2\n");
        Py_DECREF(result);
        result = NULL;
        /* Set exception if not called by gen_iternext() */
        if (arg)
            PyErr_SetNone(PyExc_StopIteration);
    }

    if (!result || f->f_stacktop == NULL) {
        fprintf(stderr, "here3\n");
        /* generator can't be rerun, so release the frame */
        Py_DECREF(f);
        gen->gi_frame = NULL;
    }
    fprintf(stderr, "return result\n");
    return result;
}

看起来生成器对象是它自己 Frame 的控制器,叫做 gi_frame。

我添加了一些 fprintf (...),所以让我们运行代码。

f.next()

f->f_lasti
here
call here
return result
1

首先,它会进入 f_lasti(这是一个整数,表示最后执行的字节码指令的偏移量,初始化为 -1),是的,它是 -1,但没有参数,然后函数继续执行。

然后跳到 here,现在最重要的是 PyEval_EvalFrameEx。PyEval_EvalFrameEx 实现了 CPython 的评估循环,我们可以认为它运行每一段代码(实际上是 Python 的操作码),并执行 print 'call here',它会打印文本。

当代码运行到 yield 时,Python 会通过使用 frame 对象来存储上下文(我们可以查看调用栈)。返回值并放弃代码的控制权。

一切完成后,return result,并在终端显示值 1

下一次我们运行 next(),它不会再进入 f_lasti 的范围。它显示:

frame
here
call here
return result
2

我们没有发送参数,所以仍然从 PyEval_EvalFrameEx 获取结果,结果是 2。

撰写回答