如何在Python C代码中使用yield,good&坏的部分

时间:2014-07-01 01:53:23

标签: python c yield

最近我一直在研究Python的代码。我知道如何使用生成器(接下来,发送等等),但通过阅读Python C代码来理解它很有趣。

我在Object/genobject.c中找到了代码,并不难理解(但仍然不容易)。所以我想知道它是如何工作的,并确保我对Python中的生成器没有误解。

我知道所有的电话

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

并且结果从PyEval_EvalFrameEx返回,看起来它是一个动态帧结构,我能理解为stack还是什么?

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

我知道如果我要解析大循环或大数据,产量是惊人的,它会节省大量内存并使其变得简单。但我的一些同事喜欢到处使用产量,就像回归一样。阅读和理解代码并不容易,并且Python存储了大多数可能永远不会被再次调用的函数的上下文。这是一种不好的做法吗?

所以,问题是:

  1. PyEval_EvalFrameEx如何运作。
  2. 记忆使用产量。
  3. 在任何地方使用产量都是不好的做法。
  4. 我发现如果我有一个生成器,函数gen_send_ex将被调用两次,为什么?

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

    它会两次调用gen_send_ex,第一次没有args,第二次调用args,然后得到结果。

    感谢您的耐心等待。

1 个答案:

答案 0 :(得分:1)

我看到了这些文章:

本文告诉我PyEval_EvalFrameEx是如何工作的。

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

本文告诉我Python中的框架结构。

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);                                                 
}

只有两个函数的区别是arg,send是发送arg,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;
}

看起来Generator Object是它自己的Frame的控制器,它叫做gi_frame。

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

f.next()

f->f_lasti
here
call here
return result
1

所以,首先它转到f_lasti(这是执行的最后一条指令的字节代码的整数偏移,初始化为-1),是的它是-1,但没有args,那么函数去上。

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

当代码转到yield时,Python使用框架对象存储上下文(我们可以搜索调用堆栈)。给予价值并放弃对代码的控制。

完成所有操作后,再return result,并在终端显示值1

下次我们运行next()时,它不会转到f_lasti范围。它显示:

frame
here
call here
return result
2

我们没有发送arg所以仍然从PyEval_EvalFrameEx得到结果,结果是2。