Глава 8. Цикл расчёта

Содержание

Глава 8. Цикл расчёта
Относящиеся к делу исходные файлы
Важные термины
Построение состояния потока
Тип состояния потока
Относящиеся к делу исходные файлы
Построение объектов кадров
Тип объекта кадра
Относящиеся к делу исходные файлы
API инициализации объекта кадра
Преобразование параметров ключевых слов в словарь
Преобразование позиционных аргументов в переменные
Упаковка позиционных аргументов в *args
Загрузка аргументов ключевых слов
Добавление пропущенных позиционных аргументов
Добавление пропущенных аргументов ключевых слов
Свёртывание замыканий
Создание генераторов, сопрограмм и асинхронных генераторов
Исполнение кадра
Отслеживание выполнения кадра
Стек значений
Пример операции байтового кода: BINARY_OR
Имитация стека значений
Действия стека
Пример: Добавление элемента в список
Выводы

До сих пор вы наблюдали как выполняется синтаксический разбор кода Python в дерево абстрактного синтаксиса и оно компилируется в объекты кода. Эти кодовые объекты содержат списки дискретных операций в форме байтового кода.

Существует один важный пропущенный момент для этих кодовых объектов для исполнения и приведения их в действие: им требуются входные данные. В Python эти входные данные получаются из локальных или глобальных переменных.

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

Выполнение кода в CPython происходит внутри центрального цикла с названием цикла расчёта (evaluation loop). Интерпретатор CPython будет рассчитывать и выполнять кодовый объект, выбираемый из либо выстроенного файла .pyc или из самого компилятора:

 

Рисунок 8-1



В цикле расчёта каждая из инструкций байтового кода берётся и исполняется при помощи стековой системы на основе кадров.

[Замечание]Замечание

Кадры стека это применяемый многими средами времени выполнения тип данных, не только Python. Стековые кадры позволяют вызов функций и возврат переменных между функциями. Кадры стека также содержат аргументы, локальные переменные, также прочие сведения с учётом состояния.

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


Traceback (most recent call last):
  File "example_stack.py", line 8, in <module>  <--- Frame
    function1()
  File "example_stack.py", line 5, in function1 <--- Frame
    function2()
  File "example_stack.py", line 2, in function2 <--- Frame
    raise RuntimeError
RuntimeError
 	   

Относящиеся к делу исходные файлы

Вот те исходные файлы, которые относятся к циклу расчёта:

Таблица 8-1. Относящиеся к расчёту исходные файлы
Файл Назначение

Python/ceval.c

Реалиация ядра цикла вычислений

Python/ceval-gil.h

Определение GIL и алгоритма управления

Важные термины

Вот некоторые важные термины, которыми вы будете пользоваться в этой главе:

  • Цикл вычислений будет получать объект кода и преобразовывать его в последовательность объектов кадров.

  • Сам интерпертатор обладает по крайней мере одним потоком.

  • Каждый поток имеет состояние потока

  • Кадровые объекты выполняются в стеке, носящем название стека кадров.

  • Стек значений содержит ссылки на переменные.

Построение состояния потока

Прежде чем кадр сможет исполняться, ему требуется присоединиться к некому потоку. CPython способен обладать большим числом потоков, запускаемых в любое время внутри единого интерпретатора. Состояние интерпретатора содержит присоединённый список таких потоков.

CPython всегда обладает хотя бы одним потоком, причём каждый поток обладает своим собственным состоянием.

[Совет]Смотри также

Более подробно построение потоков обсуждается в Главе 10, Параллельность и одновременность.

  Тип состояния потока

Тип состояния потока обладает тридцатью свойствами, включая следующие:

  • Уникальный идентификатор

  • Присоединённый список на прочие состояния потока

  • Состояние интерпертатора, из которого о был порождён

  • Исполняемый в данный момент кадр

  • Текущая глубина рекурсии

  • Не обязательные функции отслеживания

  • Обрабатываемая прямо сейчас исключительная ситуация

  • Все подлежащие в данный момент асинхронные исключительные ситуации

  • Некий стек исключительных ситуаций, возбуждённых при возведении множественных исключительных ситуаций (например, внутри блока except)

  • Счётчик GIL

  • Асинхронный генератор счётчиков

  Относящиеся к делу исходные файлы

Относящиеся к состоянию потока исходные файлы разбросаны по множеству файлов:

Таблица 8-2. Относящиеся к состоянию потока исходные файлы
Файл Назначение

Python/thread.c

Реализация API потока

Include/threadstate.h

Некоторые API состояния и определения типов

Include/pystate.h

API состояния интерпретатора и определения типов

Include/pythread.h

API построения потоков

Include/cpython/pystate.h

Некоторые API состояния и потока и интерпретатора

Построение объектов кадров

Скомпилированные объекты кодов вставляются в объекты кадров. Объекты кадров являются типом Python, а потому на них можно ссылаться как из C, так и из Python.

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

  Тип объекта кадра

Типом объекта кадра выступает PyObject со следующими дополнительными свойствами:

Таблица 8-3. Относящиеся к состоянию потока исходные файлы
Поле Тип Назначение

f_back

PyFrameObject *

Указатель на предыдущий кадр в стеке или NULL для самого первого кадра

f_blockstack

PyTryBlock[]

Последовательность блоков for, try и loop

f_builtins

PyObject * (dict)

Символьная таблица для модуля builtin

f_code

PyCodeObject *

Подлежащий исполнению кодовый объект

f_executing

char

Флаг того исполняется ли всё ещё данный кадр

f_gen

PyObject *

Заимствованная ссылка на генератор, или NULL

f_globals

PyObject * (dict)

Глобальная символьная таблица (PyDictObject)

f_iblock

int

Индекс этого кадра в f_blockstack

f_lasti

int

Последняя инструкция, если она вызывается

f_lineno

int

Текущий номер строки

f_locals

PyObject *

Локальная символьная таблица (все соответствия)

f_localsplus

PyObject *[]

Соединение locals плюс stack

f_stacktop

PyObject **

Следующий свободный слот в f_valuestack

f_trace

PyObject *

Указатель на некую индивидуальную функцию отслеживания (см. раздел Отслеживание выполнения кадра)

f_trace_lines

char

Включает пользовательскую функцию отслеживания для трассировки на уровне строки

f_trace_opcodes

char

Включает пользовательскую функцию отслеживания для трассировки на уровне кода операции

f_valuestack

PyObject **

Указатель на самый последний локальный элемент

  Относящиеся к делу исходные файлы

Вот те исходные файлы, которые относятся к объектам кадра:

Таблица 8-4. Относящиеся к объектам кадров исходные файлы
Файл Назначение

Objects/frameobject.c

Реалиация и API Python кадра объекта

Include/frameobject.h

API объекта кадра и определение типа

  API инициализации объекта кадра

API для инициализации объекта кадра, PyEval_EvalCode(), выступает точкой входа для вычисления объекта кода. PyEval_EvalCode() является обёрткой вокруг внутренней функции _PyEval\_EvalCode().

[Замечание]Замечание

PyEval_EvalCode() это сложная функция, которая определяет большое число поведений как объектов кадров, так и самого цикла интерпретатора. Именно она очень важна для понимания, ибо она также способна обучить вас некоторым принципам проектирования интерпретатора CPython.

В этм разделе вы пошагово пройдёте логикой _PyEval\_EvalCode().

PyEval_EvalCode() определяет большое число аргументов:

  • tstate: PyThreadState * указывает на состояние потока самого потока данного кода, который будет вычисляться

  • _co: PyCodeObject* содержит тот код, который должен быть помещён в объект кадра

  • globals: PyObject* (dict) с названиями ключей в качестве ключей и их значениями

  • locals: PyObject* (dict) с названиями ключей в качестве ключей и их значениями

[Замечание]Замечание

В Python локальные и глобальные переменные хранятся в каче тве словарей. Вы можете получать доступ к этим словарям при помощи встроенных функций cals() и globals():


>>> a = 1
>>> print(locals()["a"])
1
		

Все прочие аргументы не обязательные и не применяются в базовом API:

  • argcount: Значение числа позиционных аргументов

  • args: PyObject* (tuple) с со значениями позиционных аргументов по порядку

  • closure: Кортеж со строками для слияния с полем co_freevars соответствующего кодового объекта

  • defcount: Значение длины установленных по умолчанию значений для позиционных аргументов

  • defs: Перечень значений по умолчанию для позиционных аргументов

  • kwargs: Список значений аргументов кодового слова

  • kwcount: Значение числа аргументов кодового слова

  • kwdefs: Словарь со значниями по умолчанию для аргументов кодового слова

  • kwnames: Список имён аргументов кодового слова

  • name: Название для данного вычисляемого предложения в виде строки

  • qualname: Полностью определённое название для данного вычисляемого предложения в виде строки

Вызов _PyFrame_New_NoTrack() создаёт новый кадр. Этот API также доступен из API C через PyFrame_New(). _PyFrame_New_NoTrack() создаст новый PyFrameObject следуя таким шагам:

  1. Установит значение свойства f_back равным состоянию потока последнего кадра.

  2. Загрузит необходимые текущие встроенные функции через установку свойства f_builtins и загрузит сам модуль builtins при помощи PyModule_GetDict().

  3. Установит значение свойства f_code на подлежащий вычислению кодовый блок.

  4. Установит значение свойства f_valuestack на некий пустой стек значений

  5. Установит значение указателя f_stacktop на f_valuestack

  6. Установит значение глобального свойства, f_globals, на значение аргумента globals

  7. Установит значение локального свойства, f_locals, на некий новый словарь

  8. Установит значение f_lineno равным свойству co_firstlineno кодового объекта с тем, чтобы трассировщик содержал номера строк

  9. Установит значения всех остающихся свойств в их значения по умолчанию

Обладая новым экземпляром PyFrameObject, могут быть построены все аргументы для его кодового объекта:

 

Рисунок 8-2



  Преобразование параметров ключевых слов в словарь

Определения функций могут содержать вместилище всего **kwargs в качестве аргументов ключевых слов:


def example(arg, arg2=None, **kwargs):
    print(kwargs["x"], kwargs["y"])  # resolves to a dictionary key
example(1, x=2, y=3)  # 2 3
 	   

В такой ситуации создаётся некий новый словарь и выполняется повсеместное копирование не подвергшихся разрешению аргументов. Имя kwargs затем устанавливается в качестве локальной сферы действий данного кадра.

  Преобразование позиционных аргументов в переменные

Все имеющиеся позиционные аргументы (если они предоставлены) устанавливаются в качестве локалных переменных. В Python аргументы функций уже локальные переменные внутри тела этой функции. Когда определён со значением некий позиционный аргумент, он доступен внутри сферы действий этой функции:


def example(arg1, arg2):
    print(arg1, arg2) 
example(1, 2)  # 1 2
 	   

Счётчик ссылок инкрементально увеличивается для этих переменных, поэтому имеющийся сборщик мусора не сможет удалить их пока вычисляется данный кадр, например, пока эта функция не будет завершена и не вернёт результат.

  Упаковка позиционных аргументов в *args

Как и в случае с **kwargs, аргумент функции с предшествующей ему * может быть установлен в захват всех остающихся позиционных аргументов. Этот аргумент является кортежем, а имя *args устанавливается в качестве локальной переменной:


def example(arg, *args):
    print(arg, args[0], args[1])

example(1, 2, 3)  # 1 2 3
 	   

  Загрузка аргументов ключевых слов

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

Например, когда аргумент e является ни позиционным, не именованным, он добавляется в **remaining:


>>> def my_function(a, b, c=None, d=None, **remaining):
       print(a, b, c, d, remaining)

>>> my_function(a=1, b=2, c=3, d=4, e=5)
(1, 2, 3, 4, {"e": 5}) 
		
[Замечание]Замечание

Исключительно позиционные аргументы (Positional-only arguments) являются новой функциональной возможностью в Python 3.8. Введённые в PEP 570, исключительно позиционные аргументы представляют собой способ прекращения пользователями вашего API применения позиционных аргументов в синтаксисе ключевых слов.

Например, эта простая функция преобразовывает градусы по Фаренгейту в градусы по Цельсию. Обратите внимание на использование прямого слэша (/) в качестве особого аргумента, который отделяет исключительно позиционные аргументы от прочих аргументов:


def to_celsius(fahrenheit, /, options=None):
    return (fahrenheit-32)*5/9
 	   

Все аргументы слева от / обязаны вызываться исключительно как позиционные аргументы. Аргументы справа могут вызываться и как позиционные, и как аргументы с ключевыми словами:


>>> to_celsius(110)
		

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


>>> to_celsius(fahrenheit=110)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: to_celsius() got some positional-only arguments
 passed as keyword arguments: 'fahrenheit'
		

Собственно разрешение значений словаря аргументов ключевых слов происходит после распаковки всех аргументов. PEP 570 исключительно позиционные аргументы отображаются запуском цикла аргументов ключевых слов начиная с co_posonlyargcount. Если символ / применялся в качестве третьего аргумента, тогда значением co_posonlyargcount будет равен 2.

PyDict_SetItem() вызывается для каждого из оставшихся аргументов для их добавления в словарь locals. После его исполнения все аргументы ключевых слов войдут в сферу действия локальных переменных.

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


def example(arg1, arg2, example_kwarg=None):
    print(example_kwarg)  # example_kwarg is already a local variable.
 	   

  Добавление пропущенных позиционных аргументов

Все предоставляемые вызову функции позиционные аргументы, которые не находятся в списке позиционных аргументов добавляются в кортеж *args. Если такой кортеж не существует, тогда возбуждается исключительная ситуация.

  Добавление пропущенных аргументов ключевых слов

Все пердоставляемые вызову функции аргументы ключевых слов, не находящиеся в списке именованных аргументов ключевых слов добавляются в словарь **kwargs. Если такой словарь не существует, тогда возбуждается исключительная ситуация.

  Свёртывание замыканий

Все названия замыканий добавляются в список свободных имён переменных объекта кода.

  Создание генераторов, сопрограмм и асинхронных генераторов

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

[Совет]Смотри также

API и реализация кадров генераторов, сопрограмм и асинхронных кадров обсуждаются в Главе 10, Параллельность и одновременность.

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

Наконец, с новым кадром вызывается _PyEval_EvalFrame().

Исполнение кадра

Как уже обсуждалось ранее в Главе 6, Лексический и синтаксический разбор при помощи синтаксических деревьев и в Главе 7, Собственно компилятор объект кода содержит бинарное кодирование подлежащих исполнению байтовых кодов. Он также содержит перечень переменных и и символическую таблицу.

Значения локальных и глобальных переменных определяются во время исполнения на основании того как вызывались функции, модуль или блок. Эти сведения добавляются в соответствующий кадр через _PyEval_EvalCode().

Имеются и иные применения кадров, такие как декоратор сопрограммы, который динамически вырабатывает некий кадр с соответствующей целью в качестве переменной.

Общедоступный API, PyEval_EvalFrameEx(), вызывает настроенный интерпретатором кадр вычисляемой функции в свойстве eval_frame. Вычисление кадра было сделано подключаемым в Python 3.7 через PEP 523.

_PyEval_EvalFrameDefault() является установленной по умолчанию функцией вычисления и единственным поставляемым с CPython вариантом.

Эта центральная функция объединяет всё воедино и воспроизводит ваш код к жизни. Она содержит десятилетия оптимизации, ибо даже одна строка кода способна оказать значительное воздействие на на производительность CPython в целом.

Всё что подлежит исполнению через CPython проходит через эту функцию вычисления.

[Замечание]Замечание

Некоторые из вас при чтении Python/ceval.c могли заметить насколько много раз применялись макросы C..

Макросы C это некий способ обладания повторно применяемым кодом без накладных расходов выполнения вызова функции..

В Visual Studio Code идущие по ходу дела расширения макро показываются после того как вы установили официальное расширение C/C++:

 

Рисунок 8-3



В CLion выберите макро и нажмите Alt + Space, чтобы подцепить его определение.

  Отслеживание выполнения кадра

Вы можете пошагово проследовать за исполнением Python 3.7 и выше включив атрибут отслеживания в своём текущем потоке. Тип PyFrameObject содержит свойство f_trace с типом PyObject *. Ожидается, что это значение указывает на некую функцию Python.

Этот код примера устанавливает глобальную функцию отслеживания в функцию с названием my_trace(), которая получает в качестве стека стек из текущего кадра, выводит на печать в экране дизассемблированные коды операций и добавляет некоторые дополнительные сведения для отладки, cpython-book-samples/31/my_trace.py:


import sys
import dis
import traceback
import io

def my_trace(frame, event, args):
   frame.f_trace_opcodes = True
   stack = traceback.extract_stack(frame)
   pad = "   "*len(stack) + "|"
   if event == "opcode":
      with io.StringIO() as out:
         dis.disco(frame.f_code, frame.f_lasti, file=out)
         lines = out.getvalue().split("\n")
         [print(f"{pad}{l}") for l in lines]
   elif event == "call":
      print(f"{pad}Calling {frame.f_code}")
   elif event == "return":
      print(f"{pad}Returning {args}")
   elif event == "line":
      print(f"{pad}Changing line to {frame.f_lineno}")
   else:
      print(f"{pad}{frame} ({event} - {args})")
   print(f"{pad}----------------------------------")
   return my_trace
sys.settrace(my_trace)

# Run some code for a demo
eval('"-".join([letter for letter in "hello"])')
 	   

sys.settrace() установит функцией отслеживания по умолчанию текущего состояния потока ту, которая предоставлена. Все создаваемые после этого вызова кадры будут обладать f_trace, установленной в эту функцию.

Этот фрагмент кода выводит на печать сам код внутри каждого стека и перед тем как выполнить её, указывает следующую операцию. После того как кадр возвращает значение, выводится на печать это возвращаемое предложение:

 

Рисунок 8-4



Полный перечень возможных инструкций байтового кода доступен в документации модуля dis.

Стек значений

Внутри ядра цикла вычисления создаётся стек значений. Этот стек является списком указателей на экземпляры PyObject. Они могут быть такими значениями как переменные, ссылки на функции (которые являются объектами Python) или иные прочие объекты Python.

Инструкции байтового кода в цикле вычислений будут получать входные данные из этого стека значений.

  Пример операции байтового кода: BINARY_OR

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

Например, допустим мы вставили некую операцию or в Python:


if left or right:
    pass
 	   

Наш компилятор скомпилирует эту операцию or в инструкцию BINARY_OR:


static int
binop(struct compiler *c, operator_ty op)
{
    switch (op) {
    case Add:
        return BINARY_ADD;
    ...
    case BitOr:
        return BINARY_OR;
 	   

В нашем цикле вычислений вариант для BINARY_OR получит два значения из стека значений, операции left и right, а затем вызовет PyNumber_Or для этих двух объектов:


...
    case TARGET(BINARY_OR): {
        PyObject *right = POP();
        PyObject *left = TOP();
        PyObject *res = PyNumber_Or(left, right);
        Py_DECREF(left);
        Py_DECREF(right);
        SET_TOP(res);
        if (res == NULL)
            goto error;
        DISPATCH();
    }
 	   

Полученный результат, res, затем устанавливается вершиной этого стека, перекрывая текущее значение вершины.

  Имитация стека значений

Чтобы понять цикл вычислений, вам придётся разобраться c его стеком значений.

Один из способов представлять себе стек значений это рассматривать его как деревянный колышек, на который вы можете штабелировать цилиндры. При таком подходе вы могли бы добавлять или удалять лишь по одному цилиндру за раз и всегда только в его вершину или с неё.

В CPython вы можете вы можете добавлять объекты в стек значений при помощи макро PUSH(a), где a это указатель на PyObject.

Например, допустим, вы создали PyLong со значением 10 и поместите его в свой стек значений:


PyObject *a = PyLong_FromLong(10);
PUSH(a);
 	   

Это действие имело бы такой эффект:

 

Рисунок 8-5



В своей следующей операции, для выборки этого значения, вы бы воспользовались макро POP() для получения значения из вершины стека:


PyObject *a = POP(); // a is PyLongObject with a value of 10
 	   

Это действие вернёт верхнее значение и завершится опустошением стека:

 

Рисунок 8-6



Теперь, допустим, вы добавили в стек два значения:


yObject *a = PyLong_FromLong(10);
PyObject *b = PyLong_FromLong(20);
PUSH(a);
PUSH(b);
 	   

Это бы завершилось в порядке такого добавления, а потому a оказалось бы помещённым на второй позиции в стеке:

 

Рисунок 8-7



Если бы вы выполнили выборку с вершины стека, вы бы получили указатель на b, потому что он находился в вершине:


PyObject *val = POP(); // returns ptr to b
 	   
 

Рисунок 8-8



Если вам требуется выбрать значение указателя из стека без его выталкивания, тогда вы можете воспользоваться операцией PEEK(v), где v это значение позиции в стеке:


PyObject *val = POP(); // returns ptr to b
 	   

0 представляет вершину стека, 1 будет представлять вторую позицию:

 

Рисунок 8-9



Для клонирования своего значения в вершине стека вы можете применять макро DUP_TOP():


DUP_TOP();
 	   

Это действие скопирует значение в вершине для формирования двух указателей на один и тот же объект:

 

Рисунок 8-10



Макро обращения ROT_TWO поменяет местами первое и второе значения:


ROT_TWO();
 	   

Это действие переключит порядок первого и второго значений:

 

Рисунок 8-11



  Действия стека

Каждая из этих кодовых операций обладает предопределённым воздействием на стек, вычисляемым stack_effect() внутри Python/compile.c. Эта функция возвращает значение приращения в числе значений внутри стека значений для каждого кода операции.

Действия стека обладают положительным, отрицательным или нулевым значением. После того как операция осуществлена, если произошедшее воздействие на стек (например, +1) не соответствует значению приращения в стеке значений, возбуждается исключительная ситуация.

Пример: Добавление элемента в список

В Python, когда вы создаёте некий список, в этом перечне объектов доступен метод append():


my_list = []
my_list.append(obj)
 	   

В данном примере obj выступает неким объектом, который вы желаете добавить в конец своего списка.

В это действие вовлечены две операции:

  1. LOAD_FAST для загрузки obj в вершину стека значений из списка locals данного кадра

  2. LIST_APPEND для добавления этого объекта

В LOAD_FAST вовлечено пять шагов:

  1. Указатель на obj загружается из GETLOCAL(), где значение загружаемой переменной это аргумент операции. Весь список указателей на переменные хранится в fastlocals, которая является копией атрибута PyFrame из f_localsplus. Аргументом этой операции выступает число указаний на значение индекса в самом массиве указателей fastlocals. Это означает, что Python загружает локальные элементы как копии самих указателей, вместо того чтобы принимать во внимание значения имён переменных.

  2. Если такая переменная более не существует, возбуждается ошибочная ситуация несвязанной локальной переменной.

  3. Значения счётчика ссылок для value (в нашем случае для obj) увеличиваются на единицу.

  4. Значение указателя на obj помещается в вершину стека значений.

  5. Вызывается макро FAST_DISPATCH. Если включено отслеживание, тогда цикл выполняется ещё раз для всех отслеживаний. Когда трассировка отключена, для fast_next_opcode вызывается goto. Этот goto выполняет безусловный переход обратно в вершину нашего цикла за следующей инструкцией.

Вот эти пять шагов в LOAD_FAST:


... 
    case TARGET(LOAD_FAST): {
        PyObject *value = GETLOCAL(oparg);                 // 1.
        if (value == NULL) {
            format_exc_check_arg(
                PyExc_UnboundLocalError,
                UNBOUNDLOCAL_ERROR_MSG,
                PyTuple_GetItem(co->co_varnames, oparg));
            goto error;                                    // 2.
        }
        Py_INCREF(value);                                  // 3.
        PUSH(value);                                       // 4.
        FAST_DISPATCH();                                   // 5.
    }
 ...
 	   

Наш указатель на obj теперь в вершине стека значений и исполняется следующая инструкция, LIST_APPEND.

Многие операции байтового кода ссылаются на базовые типы, такие как PyUnicode или PyNumber. Например, LIST_APPEND добавляет в конец списка некий объект. Для достижения этого он вытаскивает значение указателя из стека значений и возвращает значение указателя на самый последний объект в своём стеке.

Такой макро является ярлыком для следующего:


PyObject *v = (*--stack_pointer);
 	   

Теперь указатель на obj хранится как obj. Полный список указателей загружается из PEEK(oparg).

Затем для list и v вызываются списки API C для Python. Соответствующий код для этого находится внутри Objects/listobject.c, который вы изучите в Главе 11, Объекты и типы.

Затем делается вызов PREDICT, который предвидит, что следующей операцией будет JUMP_ABSOLUTE. Макро PREDICT обладает вырабатываемым компилятором goto для каждого из потенциальных операций предложения case.

Это означает, что наш ЦПУ способен выполнить безусловный переход к таким инструкциям и ему нет надобности снова выполнять проход по этому циклу:


...
        case TARGET(LIST_APPEND): {
            PyObject *v = POP();
            PyObject *list = PEEK(oparg);
            int err;
            err = PyList_Append(list, v);
            Py_DECREF(v);
            if (err != 0)
                goto error;
            PREDICT(JUMP_ABSOLUTE);
            DISPATCH();
        }
 ...
 	   
[Замечание]Замечание

Некоторые коды операций ходят парами, чт делает возможным предсказание второй операции при исполнении первой. К примеру, за COMPARE_OP зачастую следует POP_JUMP_IF_FALSE или POP_JUMP_IF_TRUE.

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

  1. Придерживаться включённых предсказаний и воспринимать эти результаты как если бы некие коды операций сочетались.

  2. Отключить предсказания с тем, чтобы значения счётчика частоты кодов операций обновлялись для обоих кодов операций.

Для кода при многопоточности предсказание операций отключено, так как многопоточность позволяет самому ЦПУ записывать отдельные сведения предсказания ветвлений для каждого кода операции.

Некоторые из операций, например, CALL_FUNCTION и CALL_METHOD, обладают некими аргументами операции, которые ссылаются на другие скомпилированные функции. В таком случае в этом потоке в имеющийся стек кадров вставляется другой кадр и цикл вычисления выполняется для этой функции, пока эта функция не завершится.

При каждом создании нового кадра и его вставке в стек, соответствующее значение f_back этого кадра устанавливается на значение текущего кадра перед тем как будет создан новый кадр. Такое вложение кадров становится понятным когда вы просматриваете отслеживание стека, cpython-book-samples/31/example_stack.py:


def function2():
  raise RuntimeError

def function1():
  function2()

if __name__ == "__main__":
  function1()
 	   

Вызов этого из командной строки выдаст вам следующее:


$ ./python example_stack.py

Traceback (most recent call last):
  File "example_stack.py", line 8, in 
    function1()
  File "example_stack.py", line 5, in function1
    function2()
  File "example_stack.py", line 2, in function2
    raise RuntimeError
RuntimeError
		

В Lib/traceback.py для отслеживания вы можете воспользоваться walk_stack():


def walk_stack(f):
    """Walk a stack yielding the frame and line number for each frame.

    This will follow f.f_back from the given frame. If no frame is given,
    the current stack is used. Usually used with StackSummary.extract.
    """
    if f is None:
        f = sys._getframe().f_back.f_back
    while f is not None:
        yield f, f.f_lineno
        f = f.f_back
 	   

Значение родителя предка (sys._getframe().f_back.f_back) устанавливается в качестве кадра, поскольку вы не желаете в своём отслеживании видеть вызов walk_stack() или print_trace(). Указатель f_back перемещается в самый верх вызовов стека.

sys._getframe() является API Python для получения значения атрибута frame текущего потока.

Вот как мог бы выглядит стек кадров с тремя кадрами, причём каждый со своим объектом кода, а состояние потока указывает на текущий кадр:

 

Рисунок 8-12



Выводы

В этой главе вы получили введение в мозг CPython. Обсуждавшееся здесь ядро цикла вычислений является основным взаимодействием между скомпилированным кодом Python и лежащими в основе модулями расширения, библиотеками и системными вызовами C.

Некоторые темя в этой главе оказались смазанными, поскольку вы вернётесь к ним в последующих главах. Например, интерпретатор CPython обладает ядром цикла вычислений, однако вы можете обладать множеством запущенных в одно и то же самое время циклов, будь то параллельная или одновременная обработка.

CPython может обладать большим числом циклов вычислений, исполняющих множество кадров в системе. В последующей главе Главе 10, Параллельность и одновременность как используется система стека кадров для того, чтобы CPython работал на множестве ядер или ЦПУ. Кроме того, API объекта кадра CPython позволяет приостанавливать кадры и возобновлять их в виде асинхронного программирования.

Загрузка переменных при помощи стек значений требует выделения памяти и управления ею. Для действенной работы CPython ему требуется основательный процесс управления памятью. В нашей следующей главе вы изучите как работает управление памятью и как она соотносится с теми указателями PyObject, которые используются циклом вычислений.