Python C代码中的yield如何工作,优缺点是什么
最近我在研究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会为大多数可能永远不会再被调用的函数存储上下文。这算不算一种不好的做法?
所以,我的问题是:
PyEval_EvalFrameEx
是怎么工作的。- yield的内存使用情况。
- 到处使用yield算不算不好的做法。
我发现如果我有一个生成器,函数gen_send_ex
会被调用两次,为什么呢?
def test():
while 1:
yield 'test here'
test().next()
它会第一次调用gen_send_ex
时不带参数,第二次带参数,然后得到结果。
谢谢你的耐心。
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。