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

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

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

Чтобы было ещё удобнее — 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())],
  34             keywords=[]))],
  35       orelse=[])],
  36   type_ignores=[])

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

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

Транслятор

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

   1 >>> compile("1 + 2", "<пример>", "eval").co_code
   2 b'\x97\x00y\x00'
   3 >>> compile("a + 2", "<пример>", "eval").co_code
   4 b'\x97\x00e\x00d\x00z\x00\x00\x00S\x00'
   5 >>> compile(tree, "<AST>", "exec").co_code
   6 b'\x97\x00d\x00Z\x00\x02\x00e\x01d\x01\xab\x01\x00\x00\x00\x00\x00\x00D\x00]\x0b\x00\x00Z\x02\x02\x00e\x03e\x02e\x00\xab\x02\x00\x00\x00\x00\x00\x00\x01\x00\x8c\r\x04\x00y\x02'
   7 

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

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

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

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

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

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

TODO Python3.13+ — изменения в формате вывода dis()

(если успеем) Бонус: минимальный 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. EJudge: PseudoQuine 'Нечестный квайн'

    Написать функцию quine(), которая возвращает строку, содержащую её собственный исходный текст на Python, то есть работает как квайн. Однако эта функция квайном не является, потому что получает свой исходный текст с помощью inspect. ☺

    Input:

       1 res = quine()
       2 exec(res, None, namespace := {})
       3 print(namespace['quine'].__code__.co_code == quine.__code__.co_code)
    
    Output:

    True
  3. EJudge: SequenceDiv 'Деление последовательностей'

    Написать программу (внимание! это именно программа, а не функция или класс), входные данные для которой — это текст на ЯП Python. Этот текст следует выполнить с помощью exec(), предварительно заменив в нём операцию целочисленного деления «//» на более «продвинутую», которая применима также к конечным индексируемым последовательностям и возвращает соответствующую долю этой последовательности, начиная с нулевого элемента. Операцию «//=» реализовывать не надо.

    Input:

       1 print(123 * 4 // 3, "123" * 4 // 3)
    
    Output:

    164 1231
  4. EJudge: SchMackross 'Макросы-Шмакросы'

    Написать класс macro со следующими свойствами:

    • @macro — декоратор, который «превращает функцию в макрос» (см. пояснения далее). Получившаяся функция должна продолжать работать.

    • @macro() — декоратор, «раскрывающий макросы» в функции: заменяет в ней все макровызовы конструкции вида макрос(фактические параметры) на тело макроса, в котором вместо формальных параметров подставлены фактические

    Для простоты соблюдаются следующие требования:

    • Макрос работает с исходным текстом функции посредством inspect.getsource()

    • Функция под декоратором @macro имеет только позиционные параметры и состоит из единственного оператора return выражение (то есть практически именованная labmda)

    • Вложенных макросов ни в макроопределениях, ни в макроподстановках нет (т. е. нельзя написать macro1(macro2(…))

    • Вложенных декораторов (вообще других декораторов) тоже нет
    • Переопределять макрос нельзя (определять несколько — можно)
    Input:

       1 @macro
       2 def sum3(a, b, c):
       3     return a * b + c
       4 
       5 
       6 @macro()
       7 def calculate(x, y):
       8     w = x + y
       9     return sum3(w + x, y, x * y)
      10 
      11 
      12 print(calculate(2, 3), sum3(2, 3, 4))
    
    Output:

    27 10

LecturesCMC/PythonIntro2024/30_DisInspect (последним исправлял пользователь FrBrGeorge 2024-11-24 17:15:48)