Магия функций генератора Python


фото Кайке Роша

Функции-генераторы — одна из самых крутых функций языка программирования Python. В Интернете есть множество статей, описывающих множество преимуществ функций генератора с точки зрения скорости, масштабируемости и эффективности памяти наших программ на Python. Тем не менее, существует не так много материала, который проливает свет на то, как на самом деле работают функции генератора за кулисами. Эта статья пытается заполнить этот пробел, проливая свет на некоторые ключевые особенности языка программирования python, которые делают возможными функции генератора.

Фундаментальная особенность, которая наделяет функции-генераторы их сверхспособностями, — это способность функции-генератора быть приостановлено а потом возобновлено в любое время из любой функции. Локальное состояние функции-генератора сохраняется после приостановки функции и доступно, когда функция снова возобновляется. Как это возможно? Как можно приостановить функцию, а затем возобновить ее локальное состояние? Насколько нам известно, функции имеют единственный входная точка и несколько Выход точки ( возвращаться заявления). Каждый раз, когда мы вызываем функцию, код выполняется, начиная с первой строки функции, пока не встретит точку выхода. В этот момент управление возвращается вызывающему функцию, а стек локальных переменных функции очищается, а соответствующая память освобождается операционной системой.

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

Чтобы попытаться понять магию функций генератора, давайте начнем с более внимательного изучения обычной функции:

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

Мы используем встроенный осмотреть модуль для захвата текущего кадра выполнения add_two_numbers функция. Ближе к концу мы печатаем объект кадра стека и любые локальные переменные, связанные с ним. Мы ожидаем, что кадр стека будет пустым и, следовательно, не будет иметь локальных переменных. Давайте продолжим и выполним приведенный выше фрагмент кода:

ЧТО?! Фрейм стека и все связанные с ним локальные переменные все еще зависают после вызова метода add_two_numbers заключает! Что здесь происходит? Наткнулись ли мы на утечку памяти в CPython? Нет, это не так. Это наблюдение приводит нас к одной из фундаментальных характеристик фреймов стека Python: Кадры стека Python не размещаются в памяти стека. Вместо этого они размещаются в куче памяти. . По сути, это означает, что кадры стека Python могут пережить соответствующие вызовы функций! Генераторные функции используют это поведение, чтобы творить чудеса.

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

Когда интерпретатор CPython увидит ГЕНЕРАТОР флаг на объекте кода, связанном с функцией, он не выполняет функцию, а вместо этого возвращает объект-генератор. Объект генератора является итератором. Это означает, что мы можем перебирать его, используя ключевое слово next или цикл for.

По мере того, как мы повторяем функцию генератора, выполнение продолжается до тех пор, пока урожай встречается утверждение. В этот момент кадр стека функции замораживается, и управление возвращается вызывающей стороне функции-генератора.

Поскольку мы продолжаем продвигаться по функции генератора, вызывая следующий или через для цикла , выполнение начинается точно с того места, где оно было остановлено в последний раз (последнее урожай заявление). Как интерпретатор CPython узнает, где в последний раз было остановлено выполнение экземпляра функции-генератора? Это известно через объект кадра стека, связанный с исполняемым экземпляром генератора.

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

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

Мы создаем простую функцию генератора, которая дает два числа. Вызов функции генератора создает и возвращает объект генератора. Во время этого вызова код в теле функции-генератора не выполняется, и указатель последней инструкции инициализируется значением -1. Когда мы начинаем выполнять функцию генератора, вызывая next, указатель последней инструкции переходит от одного оператора yield к другому (текущая позиция указатель последней инструкции после каждого вызова next, обозначенного → в приведенных выше фрагментах кода), останавливаясь, а затем возобновляя с той же точки, пока функция генератора не будет исчерпана и не будет выдано исключение StopIteration.

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

Если вы чувствуете себя авантюрным, вы можете взять на _PyEval_EvalCodeWithName функционировать в Python/ceval.c и Python/genobject.c module в исходном коде CPython, чтобы увидеть подробности реализации. Этот блог был написан с использованием исходного кода для CPython 3.6.

Если функции генератора были для вас загадкой, надеюсь, этот пост помог внести некоторую ясность в то, как работают функции генератора.

Похожие записи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *