Недавно (не так давно) мы начали переделывать все наши системы, чтобы они были совместимы с Python 3. Эти усилия были инициированы коллегой, который был преданным поклонником Python 3, подтолкнув всех нас к тому, что, как мы знали, мы должны были делать в первую очередь: к переходу на самую последнюю версию.

Я не буду на этом останавливаться, так как есть много других замечательных статей о миграции Python, за исключением того, что миграция прошла очень хорошо: единственной жертвой стала моя мышечная память, поскольку я переучил пальцы набирать print () вместо print. Это было незначительное разочарование, но, тем не менее, разочарование, и это разочарование привело к идее розыгрыша!

Не могли бы вы взломать print как выражение обратно в Python 3?

Я обсуждал это с другим своим коллегой, и у меня появился план: мы создадим индивидуальную версию Python 3, как можно скорее, к первоапрельской шутке, и мы проведем фальшивый ICO, в котором мы случайно заметим, что, пока в Python 3, кошелек для монет фактически использовал печать как заявление.

Вперед!

Я мало что знал (и до сих пор не знаю) о внутренностях Python C. Однако я знал, что для добавления print в качестве оператора обратно мне придется изменить грамматику интерпретатора cpython, поэтому я начал с этого. К счастью, в руководстве разработчика Python есть раздел о том, как это сделать:



так что я подумал, что просто пройду через это. Будучи ленивым программистом, я подумал, что самый простой путь к успеху - это просто перенести эти файлы с Python 2 на Python 3 и повторно вставить все, что упоминает печать как выражение, и надеюсь, что это сработает (Спойлеры : в итоге вроде заработало!)

Посмотрим, как все прошло!

Шаг 1: грамматика / грамматика

Эта часть была довольно простой: грамматика - это просто набор символов, описывающих различные синтаксические инструкции и форму, которой они соответствуют, поэтому компилятор знает, что делать и куда направлять каждый символ. В этом случае нужно было просто скопировать обратно в print_stmt из ветки Python 2 и не выяснять, в каком DSL записан этот файл.

(Боковая панель: у меня были небольшие проблемы с выяснением, на каком языке был фактически написан этот файл, поэтому я оставлю его здесь для потомков: он называется Расширенная форма Бэкуса-Наура, и его можно использовать для выражения любого контекста. -свободная грамматика!)

Сортировано! следующий.

Шаг 2: Parser / Python.asdl

Изменения ASDL прошли нормально - я просто скопировал и вставил их обратно из Python 2 в Python 3. tl; dr в файле ASDL здесь заключается в том, что он управляет интерпретатором AST (или абстрактным синтаксическим деревом): он переводит (я не думаю, что это правильное слово здесь, но эх) грамматика, которую мы создали выше, и вызывает соответствующий объект AST (который мы создадим на шаге 3) со значениями функции, извлеченными из оператора. Изменения ASDL действительно потребовали перехода от логического значения в определении схемы, но и здесь для наших целей будет достаточно int:

| Print(expr? dest, expr* values, int nl)

Грубо говоря, это означает, что узел «Печать» принимает необязательное выражение dest, одно или несколько значений выражений и int nl.

Но как это зовется? Объект AST, который мы создадим на шаге 3!

Шаг 3: Python / ast.c

Теперь, когда у нас есть наша контекстно-свободная грамматика и ASDL, превращающий ее в своего рода вызываемый объект, нам нужна функция, которая ее обрабатывает!

Если вам интересно, почему вы никогда больше нигде не определяете функцию печати, как это сделал я, это потому, что make regen-ast позаботился об этом на предыдущем шаге и выполнил правильную маршрутизацию и загрузку в Python-ast. c и Python-ast.h.

Этот файл представляет собой огромную массу определений операторов - именно здесь мы определяем, что мы на самом деле делаем с теми назначениями, значениями и nls, которые, как мы сказали, мы делаем на предыдущем шаге. Это вся логика, которая гласит: «Если первые два символа - ››, это цель, если нет, то вещь - значение» и т. Д.

Я определенно не собираюсь слишком много думать обо всем этом, поэтому я копирую / вставляю сюда материалы для печати из Ast.c Python 2, и в основном он работает нормально (есть несколько бит, с которыми вам нужно повозиться, например, переименованные вызовы и тот факт, что nl больше не является bool), но в остальном все работает нормально.

Успех! Переходим к шагу 4.

Шаг 4: сделайте реген-грамматику

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

Шаг 5: Python / symtable.c

Этот файл контролирует ... ну, если честно, я не особо уверен, что полностью у меня этот файл. Насколько я могу понять из прочитанного мною небольшого фрагмента, таблицы символов обычно используются для проверки типов и лексической области видимости: компилятор рекурсивно строит дерево AST, используя таблицу символов (я думаю?), И таким образом знает, какие переменные связаны с какой областью действия, что мы можем вернуть, что мы можем назначить (например, зная, что вы не можете объявить глобальную переменную внутри функции с этой переменной в качестве параметра) и т. д.

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



К счастью, нам не нужно слишком хорошо разбираться в этом, чтобы продолжить наш путь: просто вживите печатные материалы Python 2 и продолжайте дальше!

Шаг 6: Python / compile.c

В этом файле компилятор создает байт-код (файл .pyc) для запуска интерпретатора. байт-код - это просто коды операций C, которые выскакивают и добавляют элементы из стека (LOAD_FAST, ROT_TWO и т. д.). Мы просто снова сделаем танго копировать + вставить - надеюсь, нам не нужно ничего знать о том, что это значит.

Шаг 7: Python / ceval.c

Практическая сторона меня на данном этапе хочет признать поражение: просто вставьте фальшивый заголовок версии в скомпилированный интерпретатор 2.7 и закончите. Но та часть меня, которая наклоняется к ветряным мельницам, так близко!

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

Сразу для меня очевидны две вещи: я понятия не имею, что происходит, и ceval.c 2.7 значительно отличается от ceval.c 3.5.

пример:

TARGET_NOARG(INPLACE_MODULO)
        {
            w = POP();
            v = TOP();
            x = PyNumber_InPlaceRemainder(v, w);
            Py_DECREF(v);
            Py_DECREF(w);
            SET_TOP(x);
            if (x != NULL) DISPATCH();
            break;

vs 3.5:

TARGET(INPLACE_MODULO) {
            PyObject *right = POP();
            PyObject *left = TOP();
            PyObject *mod = PyNumber_InPlaceRemainder(left, right);
            Py_DECREF(left);
            Py_DECREF(right);
            SET_TOP(mod);
            if (mod == NULL)
                goto error;
            DISPATCH();
        }

в 2.7 кажется, что w, v и x - это временные глобальные регистры, которые используются повсюду, и чертовски сложно понять, что, черт возьми, они делают или которым назначены в любой момент времени. Кроме того, из-за этого блоки 2.7 TARGET могут «пропадать» до следующего, сохраняя присвоенные значения этим глобальным объектам, в то время как блоки 3.5 не имеют такой роскоши.

Я копирую и вставляю туда коды операций 2.7, чтобы посмотреть, работают ли они, и они взрываются повсюду на мне: после некоторой работы (превращения этих глобальных переменных в правильные локальные переменные, замены старых функций их переименованными эквивалентами 3.5) я получаю close, но есть одна функция, которая меня ставит в тупик: PyFile_SoftSpace. Остальные функции имеют 3,5 эквивалента, но этого нет.

int PyFile_SoftSpace(PyObject *p, int newflag)¶
This function exists for internal use by the interpreter. Set the softspace attribute of p to newflag and return the previous value. p does not have to be a file object for this function to work properly; any object is supported (thought its only interesting if the softspace attribute can be set). This function clears any errors, and will return 0 as the previous value if the attribute either does not exist or if there were errors in retrieving it. There is no way to detect errors from this function, but doing so should not be needed.

Честно говоря, я довольно много погуглил и до сих пор не уверен на 100%, что понимаю, для чего, черт возьми, эта функция. В журнале изменений версии 3.0 я нашел несколько упоминаний о программном пространстве:

The print() function doesn’t support the “softspace” feature of the old print statement. For example, in Python 2.x, print “A\n”, “B” would write “A\nB\n”; but in Python 3.0, print(“A\n”, “B”) writes “A\n B\n”.

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

Remarks
Classes that are trying to simulate a file object should also have a writable softspace attribute, which should be initialized to zero. This will be automatic for most classes implemented in Python (care may be needed for objects that override attribute access); types implemented in C will have to provide a writable softspace attribute.
Note
This attribute is not used to control the print statement, but to allow the implementation of print to keep track of its internal state.

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

Что, если бы я полностью избегал логики file и dest? Что, если бы я просто использовал байт-код PRINT_EXPR, который уже существует? (Зоркие среди вас могут предсказать, что произойдет дальше, учитывая изображения в статье!)

static int
compiler_print(struct compiler *c, stmt_ty s)
{
 int i, n;
 int dest;
assert(s->kind == Print_kind);
 n = asdl_seq_LEN(s->v.Print.values);
 dest = 0;
 if (s->v.Print.dest) {
 VISIT(c, expr, s->v.Print.dest);
 dest = 1;
 }
 for (i = 0; i < n; i++) {
 expr_ty e = (expr_ty)asdl_seq_GET(s->v.Print.values, i);
 if (dest) {
 ADDOP(c, DUP_TOP);
 VISIT(c, expr, e);
 ADDOP(c, ROT_TWO);
 ADDOP(c, PRINT_EXPR);
 }
 else {
 VISIT(c, expr, e);
 ADDOP(c, PRINT_EXPR);
 }
 }
// if (s->v.Print.nl) {
// if (dest)
// ADDOP(c, PRINT_EXPR)
// else
// ADDOP(c, PRINT_EXPR)
// }
// else if (dest)
// ADDOP(c, POP_TOP);
 return 1;
}

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

Это работает без каких-либо проблем, за исключением того, что теперь у меня есть несколько стандартных библиотек Python, которые полагаются на печать как на функцию, и они умирают, когда я пытаюсь завершить компиляцию (Поднятые моей собственной петардой!)

Как оказалось, решение этой проблемы оказалось проще, чем я думал (моей первой попыткой было неуклюжее регулярное выражение sed на месте, которое уничтожило эти файлы): я нашел 3to2, конвертер, который превращает код Python 3 в код Python 2. , и я запустил его против всех включенных в стандартную библиотеку Python (действие, которое казалось восхитительно трансгрессивным).

Я наконец запускаю компилятор, скрещиваю пальцы и ...

На всякий случай я переименовал метод печати в threeprint в файле bltin_modules.c, чтобы сохранить печать как функцию (и включить маленькое пасхальное яйцо в стиле тролля).

Фактическая демонстрация прошла очень хорошо!

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

Вы заметите шквал sudo cp вверху: как оказалось, действительно сложно установить пакеты python3 с помощью pip на этот взломанный интерпретатор, поскольку большая часть его предполагает, что print - это функция (и почему бы и нет?). Я обошел это, установив pip в глобальный интерпретатор Python 3 на виртуальной машине, а затем скопировав эти пакеты в настраиваемый интерпретатор Python, убедившись, что 3to2 все файлы, которые были заблокированы при импорте.

Заключение

Хотя это началось как забавная шутка, я на самом деле многое узнал о внутренней работе Python, и это действительно проливает свет на то, как глубокое погружение в то, как работает интерпретатор CPython (а также насколько хорошо минимализм Python помогает ему в поддержании его реализации на C довольно чистой. ).

Если кому интересно, ветка с моим взломанным оператором печати живет здесь: https://github.com/hbbtstar/cpython

И вот несколько ссылок, которые я нашел полезными для жирного шрифта:







«Внутренние элементы Python: именование
Сегодняшняя статья из нашей серии статей о внутреннем устройстве Python посвящена именованию, то есть способности связывать… tech.blog.aknin .имя"





Спасибо за чтение, и я бы попросил всех, кто знает об этом больше, чем я (а это, вероятно, все), дать комментарии, в которых я ошибаюсь, чтобы я мог их исправить!