Как работает yield в коде Python C, хорошая и плохая часть
Недавно я изучал код 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, как и return. Нелегко читать и понимать код, и Python хранит контекст для большинства функций, которые никогда не могут быть вызваны снова. Это плохая практика?
Итак, вопросы:
- Как
PyEval_EvalFrameEx
Работа. - Использование памяти по урожайности.
- Это плохая практика для использования урожайности везде.
И я нашел, если у меня есть генератор, функция 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.
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, следующая - 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, но без аргументов, затем функция продолжается.
Тогда перейдите here
Теперь самое важное - это PyEval_EvalFrameEx. PyEval_EvalFrameEx реализует цикл оценки CPython, можно предположить, что он выполняет каждый код (фактически это код операции Python), и запустить строку print 'call here'
Напечатай текст.
Когда код переходит к yield
Python хранит контекст с помощью фреймового объекта (мы можем искать Call Stack). Вернуть значение и отказаться от контроля над кодом.
После того, как все сделано, то return result
и показывая значение 1
в терминале.
В следующий раз, когда мы запустим next(), он не будет f_lasti
объем. Это показывает:
frame
here
call here
return result
2
Мы не отправляли аргумент arg, поэтому все еще получаем результат от PyEval_EvalFrameEx, и результат равен 2.