функции dis.dis и dis.show_code из него. В качестве аргумента dis.dis может принимать: строки с исходных кодом; функции, методы, классы, генераторы и прочие «запускаемые» объекты; «сырой байткод».
число 12345: >>> import dis >>> dis.dis('12345') 0 RESUME 0 1 LOAD_CONST 0 (12345) RETURN_VALUE 1. RESUME с параметром 0 обозначает начало обычной функции. 2. LOAD_CONST с параметром 0 помещает на стек данных виртуальной машины константу с индексом 0. В скобках указано её реальное значение 12345. 3. RETURN_VALUE берет значение из головы стека данных и возвращает это значение из функции. Числа слева от инструкций обозначают номера строк.
получается код функции, возвращающей это выражение. Действительно, аналогичный байткод можно получить при дизассемблировании функции без аргументов, возвращающей 12345: >>> dis.dis(lambda: 12345) 1 RESUME 0 LOAD_CONST 0 (12345) RETURN_VALUE Причина: dis.dis при запуске на строке исходного кода первым этапом компилирует код.
>>> code2 = (lambda: 12345).__code__ >>> code1.co_code == code2.co_code True Иными словами, объекты code1 и code2 имеют одинаковый байткод. Такие объекты называются code objects и являются основой всех «запускаемых» сущностей.
>>> [b for b in x.__code__.co_code] [128, 0, 82, 0, 35, 0] Каждой инструкции соответствует два байта, первый называется opcode, а второй oparg. До версии 3.6 число байт не было фиксировано и зависело от инструкции. В данном случае у нас три инструкции, потому байткод имеет 6 байт, все oparg равны 0. С помощью dis.opname убедимся, что это те самые инструкции: >>> [dis.opname[x] for x in (128, 82, 35)] ['RESUME', 'LOAD_CONST', 'RETURN_VALUE']
значения: #define RETURN_VALUE 35 ... #define LOAD_CONST 82 ... #define RESUME 128 Байткод Python часто меняется, и соответствие между числовыми значениями opcode и инструкциями тоже изменяется. Контролируется это соответствие при помощи "магического номера", который находится в Include/internal/pycore_magic_number.h: #define PYC_MAGIC_NUMBER 3626
RESUME 0 1 LOAD_CONST 0 ('42') RETURN_VALUE >>> dis.dis("42") 0 RESUME 0 1 LOAD_SMALL_INT 42 RETURN_VALUE Неожиданно вместо инструкции LOAD_CONST появилась инструкция LOAD_SMALL_INT! Она используется с целью оптимизации для загрузки на стек маленьких чисел.
LOAD_SMALL_INT 42 RETURN_VALUE >>> [b for b in y.__code__.co_code] [128, 0, 94, 42, 35, 0] У второй инструкции oparg равен 42. Косвенная адресация через кортеж констант y.__code__.co_consts не используется, значение вписано напрямую в oparg. Разумеется, это работает только с числами, которые помещаются в байт.
функцию z с измененным code object: >>> y_ba = bytearray(y.__code__.co_code) >>> y_ba[3] = 33 >>> z_code = y.__code__.replace(co_code=bytes(y_ba)) >>> import types >>> z = types.FunctionType(z_code, {}) >>> dis.dis(z) 1 RESUME 0 LOAD_SMALL_INT 33 RETURN_VALUE >>> z() 33 Прямое редактирование байткода может приводить к различным проблемам, включая аварийную остановку интерпретатора. Не используйте подобные приемы в production-коде!
(a) LOAD_NAME 1 (b) BINARY_OP 0 (+) RETURN_VALUE Здесь используются не константы, а переменные. Логика простая: сначала с помощью LOAD_NAME положили на стек значение a и значение b, а потом инструкция BINARY_OP взяла два значения с головы стека и положила обратно результат сложения.
0 LOAD_CONST 0 (' ') LOAD_ATTR 1 (join + NULL|self) LOAD_CONST 1 (('x', 'y')) CALL 1 RETURN_VALUE Причина: функции и методы, в том числе встроенные, могут меняться.
dis.dis(x) 1 RESUME 0 LOAD_SMALL_INT 42 RETURN_VALUE >>> y = lambda: 42 >>> x.__code__.co_code == y.__code__.co_code True Видно, что никакой разницы нет. Иными словами, def-нотация и lambda-нотация отличаются только синтаксически.
a. Эти значения лежат в замыкании: >>> x.__closure__ (<cell at 0x7fc0b28dfb10: int object at 0x55f13bcddbd8>,) >>> y.__closure__ (<cell at 0x7fc0b2b1aad0: int object at 0x55f13bcddbb8>,) >>> x.__closure__[0].cell_contents 2 >>> y.__closure__[0].cell_contents 1
x() 43 Замыкания можно сравнивать: >>> y.__closure__[0].cell_contents = 43 >>> x.__closure__ == y.__closure__ True При этом у каждой из функций x и y свое замыкание: >>> x.__closure__ is y.__closure__ False А code object общий: >>> x.__code__ is y.__code__ True
is gen.__code__ True В атрибуте gi_code хранится code object генераторной функции. >>> g.gi_frame <frame at 0x7f43d808fa00, file '...', line 1, code gen> Внутри gi_frame лежит связанный с генератором фрейм исполнения.
YIELD_VALUE 0 RESUME 5 POP_TOP LOAD_CONST 1 (None) RETURN_VALUE -- L2: CALL_INTRINSIC_1 3 (...) RERAISE 1 ExceptionTable: L1 to L2 -> L2 [0] lasti По сравнению с обычными функциями появились инструкции RETURN_GENERATOR и YIELD_VALUE, добавилась таблица исключений. Инструкция RETURN_GENERATOR создает и возвращает объект генератора g. При этом текущий фрейм исполнения «отцепляется» и в дальнейшем хранится непосредственно в g.
них есть своя генераторная функция, задаваемая неявно. >>> g = (x for x in range(5)) >>> list(g) [0, 1, 2, 3, 4] >>> g.gi_frame <frame at 0x7f43d7ce49f0, file '...', line 1, code <genexpr>> >>> g.gi_code <code object <genexpr> at 0x7f43d7c81690, file "...", line 1>
инструкцию LOAD_FAST! Посмотрим на информацию о code object: >>> dis.show_code(g) Name: <genexpr> Filename: <python-input-467> Argument count: 1 Positional-only arguments: 0 Kw-only arguments: 0 Number of locals: 2 Stack size: 2 Flags: OPTIMIZED, NEWLOCALS, GENERATOR Constants: 0: None Variable names: 0: .0 1: x Действительно, есть переменная с таким именем. Кроме того, присутствует флаг GENERATOR.
строится генераторное выражение. Изменим его: >>> g = (x for x in range(5)) >>> type(g.gi_frame.f_locals) <class 'FrameLocalsProxy'> >>> g.gi_frame.f_locals['.0'] = iter(range(10)) >>> list(g) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] Такой код будет работать только начиная с 3.13, в этой версии введен FrameLocalsProxy. На более старых версиях Python изменение в f_locals не приведет к реальному изменению генератора g. Потому что в качестве f_locals использовался словарь с копиями значений локальных переменных.
list comprehensions; set comprehensions; dict comprehensions. Обрабатываются они сходным образом. Посмотрим на результат дизассемблирования списочного выражения.
PEP 709 – «Inlined comprehensions», внедренный в Python 3.12. Создание отдельных code object убрали для list/set/dict comprehensions. Нет необходимости создавать промежуточный генератор. Можно обойтись итеративным наполнением списка/множества/словаря. С генераторными выражениями так не получится в силу их специфики. До полного завершения итерации генератора необходимо хранить фрейм исполнения.
друга. Посмотрим на примере двух функций: >>> import inspect >>> f1 = lambda: inspect.currentframe() >>> f1() <frame at 0x7fc0b2d17a00, file '...', line 1, code <lambda>> >>> f1().f_code is f1.__code__ True >>> f2 = lambda: f1().f_back == inspect.currentframe() >>> f2() True Функция f1 возвращает свой фрейм исполнения. А функция f2 вызывает f1 и сравнивает поле f_back ответа с текущим фреймом, то есть с фреймом исполнения f2. Иными словами, каждый вызов функции порождает новый фрейм, и в поле f_back фрейма лежит ссылка на его "родителя".
>>> g = (x for x in range(2)) >>> g.gi_frame <frame at 0x7fc0b2b96eb0, file '...', line 1, code <genexpr>> >>> g.gi_frame.f_back is None True Генераторный фрейм создается вместе с генератором инструкцией RETURN_GENERATOR и существует до тех пор, пока генератор не будет исчерпан. Код в составе этого фрейма исполняется не сразу, а по частям: каждая инструкция YIELD_VALUE останавливает исполнение и возвращает управление вызывающему фрейму. Этот механизм лежит в основе всей асинхронности современного Python. Причина: генератор, корутина и асинхронный генератор - по сути один и тот же объект.
асинхронный генератор _PyAsyncGenObject. Отличаются они только тем, какой префикс будут иметь поля: у генератора – gi_name и gi_iframe; у корутины – cr_name и cr_iframe; у асинхронного генератора – ag_name и ag_iframe. Все три объекта позволяют запускать "отсоединенный" фрейм по частям. При этом разделение сделано для разграничения сценариев использования: генератор применяется в качестве итератора; корутина используется вместе с await; асинхронный генератор – с async for.
на сокращенный пример генерации кода для цикла for: static int codegen_for(compiler *c, stmt_ty s) { ... VISIT(c, expr, s->v.For.iter); loc = LOC(s->v.For.iter); ADDOP(c, loc, GET_ITER); USE_LABEL(c, start); ADDOP_JUMP(c, loc, FOR_ITER, cleanup); ... } Аналогичным образом описывается кодогенерация для всех структур языка.
Adaptive Interpreter»). Пример: >>> def sum_range(): ... s = 0 ... for x in range(1000): ... s += x ... return s ... >>> sum_range() 499500 Важно, чтобы функция sum_range была вызвана. Адаптация не выполняется после объявления функции. Она происходит в результате исполнения кода достаточное количество раз.
варианты FOR_ITER_RANGE и BINARY_OP_ADD_INT. Адаптированный байткод лежит внутри того же code object: >>> s_code = sum_range.__code__ >>> s_code.co_code == s_code._co_code_adaptive False При этом размер в байтах совпадает: >>> len(s_code.co_code) == len(s_code._co_code_adaptive) True
и генерацию байткода. Оказывается, Си-код обработчика байткода также создан при помощи кодогенерации. Для этого используется инструмент под названием сases generator, написанный на Python. В исходном коде присутствует файл с названием Python/bytecodes.c. Но это не обычный файл с кодом на Си. Он содержит описания базовых и адаптированных инструкций.
= (PyObject *)&_PyLong_SMALL_INTS[ _PY_NSMALLNEGINTS + oparg]; value = PyStackRef_FromPyObjectImmortal(obj); } Код в фигурных скобках - это обычный код на Си, а выражение до фигурных скобок задает параметры генерации: имя инструкции, входные аргументы на стеке и различные настройки.