Помимо изучения стандартной библиотеки, всегда интересно, а иногда и полезно, знать, как язык устроен изнутри. Андрей Светлов, один из разработчиков Python, советует всем интересующимся серию статей об устройстве CPython. Представляю вам перевод первого эпизода. Если хорошо пойдёт, будут переводы остальных постов.
Мой друг однажды сказал мне: «Знаешь, для некоторых людей язык C — это просто набор макросов, который разворачивается в ассемблерные инструкции». Это было давно (для всезнаек: да, ещё до появления LLVM), но эти слова хорошо мне запомнились. Может быть, когда Керниган и Ритчи смотрят на C-программу, они на самом деле видят ассемблерный код? А Тим Бёрнерс-Ли? Может он сёрфит интернет по-другому, не так, как мы? И что, в конце концов, Киану Ривз видел в том жутком зелёном месиве? Нет, правда, что, чёрт побери, он там видел?! Эм… вернёмся к программам. Что видит Гвидо ван Россум, когда читает программы на Python?
Этот пост — первый в серии статей о внутренностях Питона. Я верю, что объяснение темы другим людям — это лучший способ разобраться в ней. И мне очень хотелось научиться видеть и понимать „жуткое зелёное месиво“, которое стоит за Python-кодом. В основном я буду писать о CPython 3-й версии, о байт-коде (я не фанат этапа компиляции), но, быть может, не обойду вниманием и многое другое, что связано с исполнением Python-кода любого вида (Unladen Swallow, Jython, Cython и т.п.). Для краткости я пишу Python, подразумевая CPython, если только не говорится другое. Также я подразумеваю POSIX-совместимую ОС или, если это важно, Linux, если не утверждается иное. Если вам интересно, как работает Питон, то советую дочитать этот пост до конца. Вам тем более следует это сделать, если вы хотите контрибьютить в CPython. А можете это сделать для того, чтобы найти ошибки, которые я допустил, посмеяться надо мной и оставить ехидные комментарии, если это ваш единственный способ проявлять свои чувства и эмоции.
Практически всё, о чём я буду писать, можно найти в исходных кодах Питона или в некоторых других хороших источниках (документация, особенно эта и эта страницы, отдельные лекции с PyCon, поиск по python-dev и т.д.). Найти можно всё, но я надеюсь, что мои усилия по объединению всех материалов в один, на который можно подписаться через RSS, облегчат ваши приключения. Я предполагаю, что читатель немного знаком с языком C; с теорией операционных систем; чуть меньше, чем никак, с ассемблером любой архитектуры; чуть больше, чем никак, с Питоном и комфортно чувствует себя в UNIX (например, легко устанавливает что-либо из исходников). Не переживайте, если вам не хватает опыта во всём этом, но и лёгкого плавания я не обещаю. Если у вас нет настроенного окружения для разработки Питона, предлагаю пройти сюда и выполнить необходимые шаги.
Давайте начнём с того, что вам, наверное, уже известно. Для понимания происходящего мне кажется удобной метафора механизмов. В случае с Питоном это несложно, потому что Питон полагается на виртуальную машину, чтобы делать то, что он делает (как и большинство интерпретируемых языков). Здесь важно правильно понимать термин «виртуальная машина»: думать следует скорее в сторону JVM, нежели VirtualBox (технически, они, по сути, одинаковы, но в реальном мире их, как правило, разделяют). Понимать этот термин проще, как мне кажется, буквально — это механизм, составленный из программ. Ваш процессор — это всего лишь сложная электронная машина, которая принимает на вход машинный код и данные, имеет состояние (регистры), и базируясь на вводных данных и текущем состоянии она выводит в память или на шину новую информацию. Понятно, да? А CPython — это механизм, собранный из программных компонентов, который имеет состояние и обрабатывает инструкции (разные реализации могут использовать разные инструкции). Механизм этот работает в процессе, где находится интерпретатор Питона. Мне нравится эта метафора с «механизмами», и я уже описал её в мельчайших подробностях.
Учтя вышесказанное, давайте оценим с высоты птичьего полёта, что происходит, когда мы запускаем такую команду:
$ python -c 'print("Hello, world!")'.
Запускается бинарник Питона, инициализируется стандартная библиотека C (это происходит при запуске практически любого процесса), вызывается main-функция (смотрите исходники ./Modules/python.c
: main
, из которой вызывается ./Modules/main.c
: Py_Main
). После некоторых подготовительных шагов (разбор аргументов, учёт переменных окружения, оценка ситуации со стандартными потоками и т.д.), вызывается ./Python/pythonrun.c
: Py_Initialize
. По большому счёту, в этой функции «создаются» и собираются части, необходимые для запуска CPython-машины, и просто «процесс» превращается в «процесс с интерпретатором Питона внутри». Помимо этого, создаются две очень важные структуры: состояния интерпретатора и состояния потока. Также создаётся встроенный модуль sys
и модуль, в котором содержатся все встроенные функции и переменные. В следующих эпизодах эти шаги будут описаны во всех подробностях.
Имея всё это, Питон ползёт одним из нескольких путей в зависимости от того, что ему скормили: выполнится строка, если передана опция -c
), выполнится модуль (опция -m
), выполнится файл (явно переданный в командной строке или переданный ядром, если Питон используется как интерпретатор скрипта) или запустится REPL (это особый случай исполнения файла, являющегося интерактивным устройством). В нашем случае, будет выполнена строка, т.к. мы передали опцию -c
. Чтобы выполнить эту строку, вызывается ./Python/pythonrun.c
: PyRun_SimpleStringFlags
. Эта функция создаёт неймспейс __main__
, в котором будет выполнена наша строчка кода (где будет храниться a
, если выполнить $ python -c 'a=1; print(a)'
? Правильно, в этом неймспейсе). После создания неймспейса, строка выполняется в нём (точнее, интерпретируется). Чтобы это произошло, для начала нужно преобразовать строку во что-нибудь понятное для машины.
Как и говорил, я не буду акцентировать внимание на парсере и компиляторе Питона. Я не знаток этих областей, меня это не сильно интересует, и, насколько я знаю, в компиляторе Питона нет какой-то особой магии, выходящей за пределы университетского курса по компиляторам. Лишь немного пройдёмся по верхам этих тем и, может быть, вернёмся чуть позже, чтобы рассмотреть некоторые особенности поведения CPython (например, оператор global, который влияет на парсер). В общем, стадии парсинга/компиляции в PyRun_SimpleStringFlags
проходят следующим образом: лексический анализ и создание дерева разбора, его преобразование в абстрактное синтаксическое дерево (AST), компиляция AST в объект кода с помощью ./Python/ast.c
: PyAST_FromNode
. Сейчас можете думать об объекте кода, как о бинарной строке, с которыми могут работать механизмы виртуальной машины Питона — теперь мы готовы к интерпретации.
У нас есть практически пустой __main__
, у нас есть объект кода, и мы хотим его исполнить. Что дальше? Всё делает строчка из ./Python/pythonrun.c
: run_mod
:
v = PyEval_EvalCode((PyObject*)co, globals, locals);
Функция принимает объект кода и неймспейсы globals
и locals
(в нашем случае, они являются вновь созданным неймспейсом __main__
), создаёт объект фрейма и исполняет его. Вернёмся к Py_Initialize
, который определяет состояние потока. Каждый питонячий поток представлен отдельной структурой состояния, которая (помимо всего прочего) указывает на стек выполняемых в текущий момент фреймов. После того, как объект фрейма создан и помещён наверх стека состояния потока, он (точнее, байт-код, на который он указывает) выполняется, операция за операцией, средствами довольно длинной функции ./Python/ceval.c
: PyEval_EvalFrameEx
.
PyEval_EvalFrameEx
принимает фрейм, извлекает коды операций (и операнды, если они имеются; мы ещё поговорим об этом) и выполняет кусочки C-кода, соответствующие кодам операций. Давайте дизассемблируем отрывок Python-кода и посмотрим, как выглядят эти «коды операций»:
>>> from dis import dis # о! удобная функция для дизассемблирования!
>>> co = compile("spam = eggs - 1", "<string>", "exec")
>>> dis(co)
1 0 LOAD_NAME 0 (eggs)
3 LOAD_CONST 0 (1)
6 BINARY_SUBTRACT
7 STORE_NAME 1 (spam)
10 LOAD_CONST 1 (None)
13 RETURN_VALUE
>>>
… даже без особых знаний, байт-код оказывается достаточно читаемым. «Загружаем» что-то с именем eggs
(откуда загружаем? откуда мы это загружаем?) и загружаем константное значение (1), затем выполняется «бинарное вычитание» (что здесь подразумевается под словом «бинарный»? что является операндами?), и так далее.
Как вы могли догадаться, переменные «загружаются» из глобального и локального неймспейсов, которые мы видели ранее, на стек операндов (не путайте со стеком исполняющихся фреймов), как раз туда, откуда бинарное вычитание их вытащит, вычтет одну из другой и положит результат обратно на стек. «Бинарное вычитание» — это вычитание одного операнда из другого (отсюда и «бинарное», т.е. здесь нет никакой связи с двоичными числами).
Вы можете сами изучить функцию PyEval_EvalFrameEx
в файле ./Python/ceval.c
. Она достаточно большая и по очевидным причинам я не буду описывать её здесь целиком, но покажу код, который исполняется при обработке операции BINARY_SUBTRACT
:
TARGET(BINARY_SUBTRACT) {
PyObject *right = POP();
PyObject *left = TOP();
PyObject *diff = PyNumber_Subtract(left, right);
Py_DECREF(right);
Py_DECREF(left);
SET_TOP(diff);
if (diff == NULL)
goto error;
DISPATCH();
}
… вытолкнуть первый операнд, взять со стека второй операнд, передать оба операнда C-функции PyNumber_Subtract
, сделать непонятный (мы потом разберёмся с этим) Py_DECREF
обоим операнда, переписать верхнее значение стека результатом вычитания и сделать какой-то DISPATCH
, если diff
не равен NULL
. Итак. Хотя мы пока и не понимаем некоторых вещей, я думаю, что вычитание двух чисел в Питоне на самом низком уровне, в целом, понятно. Но чтобы дойти до этой точки у нас ушло примерно полторы тысячи слов!
После того, как фрейм выполнен, PyRun_SimpleStringFlags
возвращает код завершения, основная функция проводит чистку (особое внимание мы уделим Py_Finalize
), деинициализируется libc
(atexit
и прочее), и процесс завершается.
Надеюсь, этот пост получился достаточно информативным, и мы впоследствие будем пользоваться им как фундаментом при обсуждении разных частей Питона. У нас ещё немало терминов, к которым нужно вернуться: интерпретатор, состояние потока, неймспейс, модули, встроенные функции и переменные, объекты кода и фреймов и те непонятные слова DECREF
и DISPATCH
из обработчика BINARY_SUBTRACT
. Также у нас есть ключевой «фантомный» термин, вокруг которого мы блуждали в этой статье, но который не называли по имени — объект. Объектная система CPython важна для понимания того, как оно всё работает, и я надеюсь, мы обстоятельно обсудим её в следующем посте.
Оставайтесь на связи.
При переводе наверняка кто-то пострадал: смыслы, термины, пресмыкающиеся. Давайте вместе делать мир лучше, пишите об ошибках в комментарии, так надёжнее.
Автор: skovorodkin