Как работают генераторы в питоне

Я новичок в Python и программировании. Генераторы слишком сложны для понимания новыми программистами. Вот моя теория функций генератора в Python:

  1. Любая функция содержит yield оператор вернет объект генератора

  2. Объект генератора - это стек, содержащий состояние

  3. Каждый раз звоню .next Метод Python извлекает состояние функции, и когда он находит другой оператор yield, он снова связывает состояние и удаляет предыдущее состояние:

Пример:

 [ 
  [state1] # Stack contains states and states contain info about the function
  [state2] # State1 will be deleted when python finds the other yield? 
 ] 

Это, конечно, может быть похоже на самую глупую теорию на земле, но простите меня, я просто новичок в кодовом слове.

Мои вопросы:

  1. Что Python внутренне делает для хранения состояний?

  2. Есть ли yield оператор добавляет состояние в стек, если оно существует?

  3. Какой доход создает внутренне? Я понимаю, что yield создает объект генератора, однако мне интересно, какие объекты генератора содержат, что заставляет их работать? они просто стек / список состояний, и мы используем .next метод для извлечения каждого состояния и Python будет вызывать функцию с индексированным состоянием автоматически, например?

1 ответ

Любая функция, содержащая оператор yield, возвращает объект-генератор.

Это правильно. Возвращаемое значение функции, содержащей yield является генератором объекта. Объект генератора является итератором, где каждая итерация возвращает значение, которое было yieldЭд из кода, поддерживающего генератор.

Объект генератора - это стек, содержащий состояние

Объект генератора содержит указатель на текущий фрейм выполнения, а также целый ряд других вещей, используемых для поддержания состояния генератора. Кадр выполнения - это то, что содержит стек вызовов для кода в генераторе.

Каждый раз, когда я вызываю метод.next, Python извлекает состояние функции, а когда находит другой оператор yield, он снова связывает состояние и удаляет предыдущее состояние.

Вроде, как бы, что-то вроде. Когда вы звоните next(gen_object)Python оценивает текущий фрейм выполнения:

gen_send_ex(PyGenObject *gen, PyObject *arg, int exc) {  // This is called when you call next(gen_object)
    PyFrameObject *f = gen->gi_frame;
    ...
    gen->gi_running = 1;
    result = PyEval_EvalFrameEx(f, exc);  // This evaluates the current frame
    gen->gi_running = 0; 

PyEval_EvalFrame это функция высшего уровня, используемая для интерпретации байт-кода Python:

PyObject * PyEval_EvalFrameEx (PyFrameObject * f, int throwflag)

Это основная, не приукрашенная функция интерпретации Python. Это буквально 2000 строк. Кодовый объект, связанный с фреймом исполнения f, выполняется, интерпретируя байт-код и выполняя вызовы по мере необходимости. Дополнительный параметр throwflag в большинстве случаев можно игнорировать - если он равен true, он вызывает немедленное генерирование исключения; это используется для методов throw() объектов генератора.

Он знает, что когда он попадает yield оценивая байт-код, он должен возвращать значение, полученное вызывающей стороной:

    TARGET(YIELD_VALUE) {
        retval = POP();
        f->f_stacktop = stack_pointer;
        why = WHY_YIELD;
        goto fast_yield;
    }

Когда вы уступаете, текущее значение стека значений кадра сохраняется (через f->f_stacktop = stack_pointer), чтобы мы могли возобновить с того места, на котором остановились, когда next называется снова. Все не генераторные функции установлены f_stacktop в NULL после того, как они закончили оценку. Поэтому, когда вы звоните next снова на объект генератора, PyEval_ExvalFrameEx вызывается снова, используя тот же указатель кадра, что и раньше. Состояние указателя будет точно таким же, каким оно было при его выдаче во время предыдущего, поэтому выполнение будет продолжено с этого момента. По сути, текущее состояние кадра "заморожено". Это описано в PEP, который представил генераторы:

Если встречается оператор yield, состояние функции блокируется, и значение [yielded] возвращается вызывающей стороне.next(). Под "замороженным" мы подразумеваем, что все локальные состояния сохраняются, включая текущие привязки локальных переменных, указатель инструкций и внутренний стек оценки: сохраняется достаточно информации, чтобы в следующий раз при вызове.next() функция могла действовать точно так же, как если бы оператор yield был просто еще одним внешним вызовом.

Вот большая часть состояния, которое поддерживает генераторный объект (взятый непосредственно из его заголовочного файла):

typedef struct {
    PyObject_HEAD
    /* The gi_ prefix is intended to remind of generator-iterator. */

    /* Note: gi_frame can be NULL if the generator is "finished" */
    struct _frame *gi_frame;

    /* True if generator is being executed. */
    char gi_running;

    /* The code object backing the generator */
    PyObject *gi_code;

    /* List of weak reference. */
    PyObject *gi_weakreflist;

    /* Name of the generator. */
    PyObject *gi_name;

    /* Qualified name of the generator. */
    PyObject *gi_qualname;
} PyGenObject;

gi_frame указатель на текущий фрейм выполнения

Обратите внимание, что все это зависит от реализации CPython. PyPy/Jython/ и т.д.. вполне может быть реализация генераторов совершенно по-другому. Я рекомендую вам прочитать исходный код объектов генератора, чтобы узнать больше о реализации CPython.

Другие вопросы по тегам