Интроспекция и байткод

Интроспекция — возможность запросить тип и структуру объекта во время выполнения программы.

Доступ к исходным текстам и стеку вызовов

Чтобы было ещё удобнее — inspect:

Бонус: python3 -m inspect [--detail] inspect

Интерпретация исходного текста

Python:

  1. Синтаксический анализатор
  2. Транслятор Python → байткод
  3. Интерпретатор байткода

Ситаксический анализатор

Написан на Си.

Поддерживается 1:1 прикладной модуль ast

   1 >>> import ast
   2 >>> tree = ast.parse("""
   3     k = 1
   4     for i in range(10):
   5         print(i, k)
   6     """)
   7 >>> print(tree)
   8 <ast.Module object at 0x7f8132980dc0>
   9 >>> print(tree.body)
  10 [<ast.Assign object at 0x7f8132981060>, <ast.For object at 0x7f8138be2c50>]
  11 >>> ast.dump(tree)
  12 "Module(body=[Assign(targets=[Name(id='k', ctx=Store())], value=Constant(value=1)), For(target=Name(id='i', ctx=Store()), iter=Call(func=Name(id='range', ctx=Load()), args=[Constant(value=10)], keywords=[]), body=[Expr(value=Call(func=Name(id='print', ctx=Load()), args=[Name(id='i', ctx=Load()), Name(id='k', ctx=Load())], keywords=[]))], orelse=[])], type_ignores=[])"
  13 >>> print(ast.dump(tree, indent=2))
  14 Module(
  15   body=[
  16     Assign(
  17       targets=[
  18         Name(id='k', ctx=Store())],
  19       value=Constant(value=1)),
  20     For(
  21       target=Name(id='i', ctx=Store()),
  22       iter=Call(
  23         func=Name(id='range', ctx=Load()),
  24         args=[
  25           Constant(value=10)],
  26         keywords=[]),
  27       body=[
  28         Expr(
  29           value=Call(
  30             func=Name(id='print', ctx=Load()),
  31             args=[
  32               Name(id='i', ctx=Load()),
  33               Name(id='k', ctx=Load())]))])])

Зачем нужно ИРЛ:

Работа с деревом

Транслятор

Исполнитель Python

   1 >>> compile("1 + 2", "<пример>", "eval").co_code
   2 b'\x95\x00g\x00'
   3 >>> compile("a + 2", "<пример>", "eval").co_code
   4 b'\x95\x00\\\x00S\x00-\x00\x00\x00$\x00'
   5 >>> compile(tree, "<AST>", "exec").co_code
   6 b'\x95\x00S\x00r\x00\\\x01"\x00S\x015\x01\x00\x00\x00\x00\x00\x00\x13\x00H\x0c\x00\x00r\x02\\\x03"\x00\\\x02\\\x005\x02\x00\x00\x00\x00\x00\x00 \x00M\x0e\x00\x00\x0b\x00 \x00g\x02'
   7 

Исполнитель — это стековая машина, данные которой — объекты Python.

Модуль dis («дизассемблер»):

«Дизассемблер» можно использовать для оценки быстродействия (стоит помнить, что инструкции имеют различное время выполнение, особенно — связанные созданием пространств имён)

Байт-код Python не предназначен для взаимодействия c приложениями на Python: он не имеет внешнего API, а внетреннее API постоянно меняется. Не используйте доступ к байт-коду для решения прикладных задач, а если используете — будьте готовы переписывать свои решения с каждым минорным релизом Python.

В качестве примера посмотрите историю редактирования этой страницы (меню «Информация» в шапке) — изменения связаны со сменой байткода между 3.12 и 3.13 (правда, это мажорные релизы).

Например, начиная с Python 3.11 часть внутренних данных, необходимых для работы операторов, хранится прямо в байт-коде, между командами:

   1 >>> dis.dis(compile("for i in range(5):\n  print(i)", "<пример>", "exec"),show_caches=True)
   2   0           RESUME                   0
   3 
   4   1           LOAD_NAME                0 (range)
   5               PUSH_NULL
   6               LOAD_CONST               0 (5)
   7               CALL                     1
   8               CACHE                    0 (counter: 0)
   9               CACHE                    0 (func_version: 0)
  10               CACHE                    0
  11               GET_ITER
  12       L1:     FOR_ITER                11 (to L2)
  13               CACHE                    0 (counter: 0)
  14               STORE_NAME               1 (i)
  15 
  16   2           LOAD_NAME                2 (print)
  17               PUSH_NULL
  18               LOAD_NAME                1 (i)
  19               CALL                     1
  20               CACHE                    0 (counter: 0)
  21               CACHE                    0 (func_version: 0)
  22               CACHE                    0
  23               POP_TOP
  24               JUMP_BACKWARD           13 (to L1)
  25               CACHE                    0 (counter: 0)
  26 
  27   1   L2:     END_FOR
  28               POP_TOP
  29               RETURN_CONST             1 (None)
  30 

(если успеем) Бонус: минимальный REPL

Как запустить интерпретатор и немного его подправить:

   1 import code
   2 import readline
   3 import subprocess
   4 
   5 class BangConsole(code.InteractiveConsole):
   6     def raw_input(self, *args, **kwargs):
   7         res = super().raw_input(*args, **kwargs)
   8         if res.startswith("!"):
   9             try:
  10                 res = subprocess.run(res[1:].strip().format(**self.locals).split())
  11             except Exception as E:
  12                 print(f"Error: {E}")
  13             return ""
  14         return res
  15 
  16 if __name__ == "__main__":
  17     BangConsole().interact()

Д/З

  1. Прочитать:
  2. TODO Задача на AST

  3. TODO Задача на inspect сигнатуру

  4. TODO (если получится, то задача на байт-код, но скорее всего нет, тогда на стек вызовов)

  5. Это «обязательная задача» из бакалаврского курса на MROC3:

    EJudge: WhatWhereWho 'Что? Где? Когда?'

    Викторина проводится по следующим правилам. Вначале участник опрашивает в заданном порядке некоторых других участников, нет ли у них ответа, причём удовлетворяется первым же вариантом. Если ответа не нашлось, он может придумать свой или признаться, что не знает. Если ответ получен, он может его скорректировать (потому что нашёл недочёт) или ответить как есть. Назовём опросным планом индивидуальный список каждого участника, по которому он опрашивает остальных. Очевидно, не всякая совокупность планов хороша:

    • например, участники могут начать спрашивать друг друга по кругу;
    • или будет выбран первый из вариантов ответа вместо скорректированного (который идёт дальше в плане).

    Впрочем,

    • если несколько участников, не спрашивая друг у друга, придумали или скорректировали ответ, годится любой из вариантов;

    • если кто-то из участников мог бы скорректировать ответ, но его спрашивать и не собирались, это тоже нормально: мало ли, отчего ему не доверяют.

    Можно ли, не противореча индивидуальным опросным планам, составить полный опросный план игрока — строгую последовательность, в которой опрашиваются участники, если вопрос задан конкретному игроку?

    • Построчно в виде «Кто_спрашивает: Кого_спрашивает_1, Кого_спрашивает_2, …» вводится список участников и их опросных планов. Если участник считает, что он и так всё знает, план может быть пустой. Последняя строка ввода — пустая. Запятых и двоеточий в именах нет, пробелы могут встречаться только внутри и только поштучно.

    • Выводится строка вида «Кого_спросили: У_кого_узнать_1,  У_кого_узнать_2, …» — полный опросный план для каждого из участников в порядке их ввода

    • Если полный опросный план для какого-то игрока невозможен, ни один из планов не выводится, а вместо этого выводится":
      • «CYCLE», если участники могут начать спрашивать друг друга по кругу

      • «UNKNOWN», если у кого-то в плане опроса встречается неизвестный участник

      • «INEFFECTIVE», если кто-то может дать нескорректированный ответ, хотя мог бы узнать скорректированный

    Input:

    Милован Ильясович Михеев: Савватий Эдгардович Моисеев, Михалыч
    Савватий Эдгардович Моисеев:
    левый какой-то:
    Михалыч: Капитон Силин
    Капитон Силин:
    Output:

    Милован Ильясович Михеев: Савватий Эдгардович Моисеев, Михалыч, Капитон Силин
    Савватий Эдгардович Моисеев: 
    левый какой-то: 
    Михалыч: Капитон Силин
    Капитон Силин:

LecturesCMC/PythonIntro2025/30_DisInspect (последним исправлял пользователь FrBrGeorge 2025-11-15 14:58:47)