最近我一直在研究Python的代码。我知道如何使用生成器(接下来,发送等等),但通过阅读Python C代码来理解它很有趣。
我在Object/genobject.c中找到了代码,并不难理解(但仍然不容易)。所以我想知道它是如何工作的,并确保我对Python中的生成器没有误解。
我知道所有的电话
static PyObject *
gen_send_ex(PyGenObject *gen, PyObject *arg, int exc)
并且结果从PyEval_EvalFrameEx
返回,看起来它是一个动态帧结构,我能理解为stack
还是什么?
好的,看起来Python在内存中存储了一些上下文(我是对的吗?)。看起来每次我们使用yield都会创建一个生成器,并将上下文存储在内存中,尽管不是所有的函数和变量。
我知道如果我要解析大循环或大数据,产量是惊人的,它会节省大量内存并使其变得简单。但我的一些同事喜欢到处使用产量,就像回归一样。阅读和理解代码并不容易,并且Python存储了大多数可能永远不会被再次调用的函数的上下文。这是一种不好的做法吗?
所以,问题是:
PyEval_EvalFrameEx
如何运作。我发现如果我有一个生成器,函数gen_send_ex
将被调用两次,为什么?
def test():
while 1:
yield 'test here'
test().next()
它会两次调用gen_send_ex
,第一次没有args,第二次调用args,然后得到结果。
感谢您的耐心等待。
答案 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。