Предисловие
Привет, читатель! Меня зовут Александр Щербанюк, и я Python-разработчик. Это первая статья цикла, который посвящен разбору внутреннего устройства CPython.
На написание этой статьи, как и на желание разобраться во внутреннем устройстве CPython, меня вдохновила книга Энтони Шоу "Внутри CPython. Гид по интерпретатору Python" (обложку см. ниже).

Крайне рекомендую ее к прочтению, если тебя интересует, что скрывается под капотом Python. Несмотря на то, что книга действительно заслуживает внимания, она не лишена недостатков, самый значительный из которых - это достаточно верхнеуровневое описание некоторых этапов работы CPython. Например, этапов предконфигурации и самой конфигурации. Именно эти "темные пятна" мотивировали меня начать разбираться с тем, как же всё-таки работает CPython.
В чем цель статей?
Цель статей - разобраться на достаточно глубоком уровне, как работает CPython. То есть, что происходит от момента ввода в консоли команды python my_script.py, до момента окончания её работы. Каждая статья будет разбирать и описывать очередной кусочек флоу работы CPython, формируя таким образом непрерывное повествование о его внутреннем устройстве и процессе работы.
Как будут выглядеть статьи?
Эта и все последующие статьи будут состоять из трех простых блоков. Первый блок - небольшое предисловие. Как минимум оно будет содержать ссылку на предыдущую статью, для удобства навигации и "сшивания" всех статей воедино. Второй блок - основное тело статьи. Оно будет содержать описание логически завершенного кусочка флоу CPython. Третий блок - итоги. Этот блок будет содержать краткие и ключевые результаты рассмотренной в статье логики CPython.
Итак, начнем.
Настройка VS Code
Чтобы начать разбираться во внутреннем устройстве CPython, необходимо скачать его исходники:
git clone https://github.com/python/cpython.git
Здесь и далее я будут использовать IDE Visual Studio Code, ввиду чего дополнительно необходимо установить официальное расширение, предназначенное для отладки и удобства навигации по исходным файлам CPython.
Затем, из корневой папки склонированного ранее репозитория необходимо выполнить команду, с помощью которой фиксируется рассматриваемая версия CPython:
git checkout 3.13
Весь цикл статей будет основываться на версии 3.13 (hash commit: 606022c55db3b0b0a226747bc928a836d613f9f6).
Далее, необходимо выполнить несколько команд для конфигурации и компиляции CPython:
./configure --prefix=$(pwd)/cross-build/macOS
make -j10 all
make install
В моем случае сборка осуществляется под платформу macOS. Команды сборки под Unix и Windows немного отличаются и могут быть найдены в Python Developer's Guide.
После успешной сборки в корневой директории появятся, в моем случае, две папки: build/ и cross-build/macOS/. Именно в последней будет расположен бинарный файл CPython, который мы будем использовать для отладки.
И последний оставшийся шаг - настройка процесса отладки в VS Code. Для этого необходимо создать файл ./.vscode/launch.json следующего содержания:
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug CPython",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/cross-build/macOS/bin/python3",
"args": ["-c", "print('Hello from CPython!')"],
"stopAtEntry": true,
"cwd": "${workspaceFolder}",
"externalConsole": false,
"MIMode": "lldb",
},
],
}
Это минимальная жизнеспособная конфигурация отладчика. Важно отметить, что в качестве значения параметра configurations[0].program используется путь до бинарника, который был собран на шаге ранее. В зависимости от платформы, на которой будет осуществляться отладка, представленные выше настройки могут отличаться. За особенностями конфигурации дебаггера для других платформ можно обратиться к официальной документации VS Code по настройке отладки C/C++.
На этом подготовительные мероприятия окончены, и можно перейти к погружению в исходный код CPython.
Первые шаги
После запуска отладки выполнение сразу же останавливается во входной точке, которой является функция main, расположенная в файле Programs/python.c:
#include "Python.h"
#ifdef MS_WINDOWS
int wmain(int argc, wchar_t **argv) {
return Py_Main(argc, argv);
}
#else
int main(int argc, char **argv) {
return Py_BytesMain(argc, argv);
}
#endif
Из листинга видно, что в зависимости от платформы вызываются разные стартовые функции: для Windows вызывается функция wmain(), а для всего прочего (Linux, MacOS, и др.) - main(). Это связано с разностью кодировок на разных платформах: в Windows для передачи аргументов командной строки используется кодировка UTF-16, а для прочих платформ (не всегда, но в подавляющем большинстве случаев) - UTF-8. Так или иначе, в аргументе argv оказываются все аргументы, переданные через командную строку, а в argc - их количество. Таким образом, после выполнения команды ./cross-build/macOS/bin/python3 -c "print('Hello from CPython!')" в argv окажется ./cross-build/macOS/bin/python3 -c "print('Hello from CPython!')", а в argc - 3.
Как устроен
argv, и почему вargcбудет3?
Если присмотреться кargvчуть пристальнее, можно заметить, что эта переменная является двумерным массивом символов, и, на самом деле, каждая его строка содержит отдельный аргумент. То есть:
argv[0] = "./cross-build/macOS/bin/python3"
argv[1] = "-c"
argv[2] = "print('Hello from CPython!')"Именно поэтому в
argcбудет лежать3.
Затем следует одна из двух платформозависимых функций (Py_Main или Py_BytesMain) из Modules/main.c:
int Py_Main(int argc, wchar_t **argv) { // для Windows
_PyArgv args = {
.argc = argc,
.use_bytes_argv = 0,
.bytes_argv = NULL,
.wchar_argv = argv
};
return pymain_main(&args);
}
int Py_BytesMain(int argc, char **argv) { // для прочих платформ
_PyArgv args = {
.argc = argc,
.use_bytes_argv = 1,
.bytes_argv = argv,
.wchar_argv = NULL
};
return pymain_main(&args);
}
Единственной задачей этих функций является формирование объекта типа _PyArgv из Include/internal/pycore_initconfig.h:
typedef struct _PyArgv {
Py_ssize_t argc;
int use_bytes_argv;
char * const *bytes_argv;
wchar_t * const *wchar_argv;
} _PyArgv;
Структура агрегирует входные параметры командной строки: их количество, разнообразие форматов (char * const */wchar_t * const *) и флаг (use_bytes_argv) того, какой из двух форматов был реально использован.
Следом, вне зависимости от платформы, вызывается функция pymain_main из Modules/main.c:
static int pymain_main(_PyArgv *args) {
PyStatus status = pymain_init(args);
if (_PyStatus_IS_EXIT(status)) {
pymain_free();
return status.exitcode;
}
if (_PyStatus_EXCEPTION(status)) {
pymain_exit_error(status);
}
return Py_RunMain();
}
Эта функция интересна тем, что в ней в первый раз используется структура PyStatus из Include/cpython/initconfig.h. Это универсальный тип, предназначенный для сохранения информации о статусах отработки различных инициализационных функций внутри CPython. Описание структуры представлено ниже:
typedef struct {
enum {
_PyStatus_TYPE_OK=0,
_PyStatus_TYPE_ERROR=1,
_PyStatus_TYPE_EXIT=2
} _type;
const char *func;
const char *err_msg;
int exitcode;
} PyStatus;
Структура состоит из следующих полей:
-
_type- тип статуса:_PyStatus_TYPE_OK- удачное завершение функции_PyStatus_TYPE_ERROR- в процессе работы функции возникла ошибка_PyStatus_TYPE_EXIT- функция запросила завершение процесса
func- имя функции, спровоцировавшей ошибку. Будет отлично отNULLтолько в случае_type = _PyStatus_TYPE_ERROR.err_msg- текст ошибки. Будет отлично отNULLтолько в случае_type = _PyStatus_TYPE_ERROR.exitcode- код, с которым должен завершиться процесс. Будет отлично отNULLтолько в случае_type = _PyStatus_TYPE_EXIT.
Популярность
PyStatus
PyStatusактивно используется как возвращаемый тип многих функций инициализации CPython. Так, его можно встретить более 1000 раз в более чем 70 различных исходных файлах CPython.
Затем, поток выполнения переходит к функции pymain_init(), определенной в Modules/main.c:
static PyStatus pymain_init(const _PyArgv *args) {
PyStatus status;
status = _PyRuntime_Initialize();
if (_PyStatus_EXCEPTION(status)) {
return status;
}
PyPreConfig preconfig;
PyPreConfig_InitPythonConfig(&preconfig);
status = _Py_PreInitializeFromPyArgv(&preconfig, args);
if (_PyStatus_EXCEPTION(status)) {
return status;
}
PyConfig config;
PyConfig_InitPythonConfig(&config);
if (args->use_bytes_argv) {
status = PyConfig_SetBytesArgv(&config, args->argc, args->bytes_argv);
}
else {
status = PyConfig_SetArgv(&config, args->argc, args->wchar_argv);
}
if (_PyStatus_EXCEPTION(status)) {
goto done;
}
status = Py_InitializeFromConfig(&config);
if (_PyStatus_EXCEPTION(status)) {
goto done;
}
status = _PyStatus_OK();
done:
PyConfig_Clear(&config);
return status;
}
}
Далее мы подробно рассмотрим назначение и интересные особенности большинства из представленных функций, погружаясь в глубокие детали их реализации лишь по необходимости.
Итоги
В статье были описаны два первых шага на пути погружения во внутренности CPython. Во-первых, была настроена IDE для пошагового выполнения CPython. Во-вторых, была найдена точка входа в CPython и рассмотрены несколько стартовых функций с используемыми в них структурами.
Мой ТГ-контакт для связи: https://t.me/AlexandrShherbanjuk