Интроспекция и байткод
Интроспекция — возможность запросить тип и структуру объекта во время выполнения программы.
В Python — из коробки, в силу открытости внутренней структуры.
__dir__() / .__dict__, hasattr(), callable(), .__class__ (и классы как объекты), …; а также аннотации
Более того, inspect.get_annotations() — единственный официальный способ получать аннотации объекта
Доступ к исходным текстам и стеку вызовов
Чтобы было ещё удобнее — inspect:
getmembers() / is*()
1 >>> import inspect 2 >>> class C: 3 """Seems to be a slot class""" 4 __slots__ = "a", "b", "c" 5 d = 100500 6 def fun(x): 7 return 8 >>> inspect.getmembers(C, inspect.isfunction) 9 [('fun', <function C.fun at 0x7f9a3ffb9ee0>)] 10 >>> inspect.getmembers(C(), inspect.ismethod) 11 [('fun', <bound method C.fun of <__main__.C object at 0x7f9a3dfdc7c0>>)] 12 >>> inspect.getmembers(C(), lambda attr: not(inspect.ismethodwrapper(attr) or inspect.isbuiltin(attr))) 13 [('__class__', <class '__main__.C'>), ('__doc__', 'Seems to be a slot class'), ('__firstlineno__', 1), ('__module__', '__main__'), ('__slots__', ('a', 'b', 'c')), ('__static_attributes__', ()), ('d', 100500), ('fun', <bound method C.fun of <__main__.C object at 0x7f64e778aa40>>)] 14
Работает, только если исходники есть, конечно
Лайфхак: inspect.cleandoc() для удаления отступов (вызывается в inspect.gedoc(), например)
- В т. ч. работа с блоком параметров
1 >>> import inspect 2 >>> def fun(a, b=1, /, c=2, *, d=3): 3 return d, c, b, a 4 >>> inspect.signature(fun) 5 <Signature (a, b=1, /, c=2, *, d=3)> 6 >>> s = inspect.signature(fun) 7 >>> s.parameters 8 mappingproxy(OrderedDict([('a', <Parameter "a">), ('b', <Parameter "b=1">), ('c', <Parameter "c=2">), ('d', <Parameter "d=3">)])) 9 >>> for key, val in s.parameters.items(): 10 print(key, val, val.kind, val.kind.description) 11 a a POSITIONAL_ONLY positional-only 12 b b=1 POSITIONAL_ONLY positional-only 13 c c=2 POSITIONAL_OR_KEYWORD positional or keyword 14 d d=3 KEYWORD_ONLY keyword-only 15
Поддерживаются аннотации
Частичный разбор: getfullargspec()
- В т. ч. работа с блоком параметров
- Разбор замыкания:
Строго говоря y в __closure__ не входит, но…
До кучи — getclasstree()
Вызовы — это стек, состоящий из кадров
- Нулевой кадр относится к текущему контексту функции, первый — к контексту вызывающей функwии и т. д.
1 import inspect 2 3 def caller(a, b): 4 global F 5 guesser(b, a * 2) 6 7 def guesser(x, y): 8 Me = inspect.stack()[0] 9 print(Me.function) 10 print(Me.frame.f_globals[Me.function]) 11 print(*Me.frame.f_locals) 12 print(inspect.signature(Me.frame.f_globals[Me.function])) 13 print(inspect.stack()[1].function) 14 # print(inspect.stack() 15 16 caller(1, 10)
inspect.stack() — стек вызовов, состоит из описателей кадра
- Имя функции для удобства есть в описателе
- В кадре есть информация о глобальных и локальных именах, которые видит функция
- В частности, мы можем получить доступ к самой этой функции и посмотреть её сигнатуру
- Чтобы узнать имя вызывающей функции, заглянем в предыдущий кадр
Бонус: python3 -m inspect [--detail] inspect
Интерпретация исходного текста
Python:
- Синтаксический анализатор
- Транслятор Python → байткод
- Интерпретатор байткода
Ситаксический анализатор
Написан на Си.
Поддерживается 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())]))])])
Если очень грубо: типы данных, имена, Call(), Store()/Load()
Зачем нужно ИРЛ:
- Проверка синтаксиса
- Структурная модель программы
В частности, FrBrGeorge/PythonCopypasteProof ☺
Модификация AST-дерева перед компиляцией — изменение семантики!
- Порождение собственного AST-дерева — собственный ЯП, «компилируемый в Python»
- …
Работа с деревом
1 >>> for obj in ast.walk(tree): 2 ... print(obj) 3 <ast.Module object at 0x7fdafe04bac0> 4 <ast.Assign object at 0x7fdafe04a3b0> 5 <ast.For object at 0x7fdafe04bd90> 6 <ast.Name object at 0x7fdafe049a80> 7 <ast.Constant object at 0x7fdafe049bd0> 8 <ast.Name object at 0x7fdafe04bdc0> 9 <ast.Call object at 0x7fdafe04acb0> 10 <ast.Expr object at 0x7fdafe049d80> 11 <ast.Store object at 0x7fdafef152d0> 12 <ast.Store object at 0x7fdafef152d0> 13 <ast.Name object at 0x7fdafe0498d0> 14 <ast.Constant object at 0x7fdafe0499f0> 15 <ast.Call object at 0x7fdafe049c30> 16 <ast.Load object at 0x7fdafef15270> 17 <ast.Name object at 0x7fdafe049ae0> 18 <ast.Name object at 0x7fdafe04ada0> 19 <ast.Name object at 0x7fdafe049720> 20 <ast.Load object at 0x7fdafef15270> 21 <ast.Load object at 0x7fdafef15270> 22 <ast.Load object at 0x7fdafef15270> 23
Пример: замена конструкции a@b в синтаксическом дереве на вызов функции rnd(a, b) в проекте argdef:
1 import ast 2 class RandMathMul(ast.NodeTransformer): 3 """AST transformer a@b → rnd(a, b).""" 4 5 def visit_BinOp(self, node): 6 """Substitute ast.MatMult with ast.Call(rnd).""" 7 if isinstance(node.op, ast.MatMult): 8 return ast.Call(func=ast.Name(id='rnd', ctx=ast.Load()), args=[node.left, node.right], keywords=[]) 9 return self.generic_visit(node) 10 11 def unmatmul(expr): 12 """Replace `a@b` to `rnd(a, b)` within string expression expr.""" 13 return ast.unparse(RandMathMul().visit(ast.parse(expr))) 14 15 def rnd(a, b): 16 return a * 2 + b 17 18 s = "12 @ 23" 19 res = unmatmul(s) 20 print(res) 21 print(eval(res))
Пример формирования AST для дальнейшей трансляции Python-ом: язык Hy
Транслятор
- Из AST тоже!
Исполнитель 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.
- Классическое фон-неймановское последовательное выполнение инструкций с GOTO
- Объекты — это их ID
- Имена — это ссылки на ID в пространствах имён (то есть опять-таки в объектах Python, потому что это словари)
- Функция — это тоже объект, а её вызов — тоже инструкция
- …
Модуль dis («дизассемблер»):
1 >>> import dis 2 >>> dis.dis(compile("1 + 2", "<пример>", "eval")) 3 0 0 RESUME 0 4 5 1 2 RETURN_CONST 0 (3) 6 >>> # Ой, это компилятор сам вычислил… 7 >>> dis.dis(compile("a + 2", "<пример>", "eval")) 8 0 0 RESUME 0 9 10 1 2 LOAD_NAME 0 (a) 11 4 LOAD_CONST 0 (2) 12 6 BINARY_OP 0 (+) 13 10 RETURN_VALUE 14 >>> dis.dis(compile("for i in range(5):\n print(i)", "<пример>", "exec")) 15 0 RESUME 0 16 17 1 LOAD_NAME 0 (range) 18 PUSH_NULL 19 LOAD_CONST 0 (5) 20 CALL 1 21 GET_ITER 22 L1: FOR_ITER 11 (to L2) 23 STORE_NAME 1 (i) 24 25 2 LOAD_NAME 2 (print) 26 PUSH_NULL 27 LOAD_NAME 1 (i) 28 CALL 1 29 POP_TOP 30 JUMP_BACKWARD 13 (to L1) 31 32 1 L2: END_FOR 33 POP_TOP 34 RETURN_CONST 1 (None) 35
- Иное:
1 >>> code = compile("for i in range(5):\n print(i)", "<пример>", "exec") 2 >>> print(dis.code_info(code)) 3 Name: <module> 4 Filename: <пример> 5 Argument count: 0 6 Positional-only arguments: 0 7 Kw-only arguments: 0 8 Number of locals: 0 9 Stack size: 4 10 Flags: 0x1000000 11 Constants: 12 0: 5 13 1: None 14 Names: 15 0: range 16 1: i 17 2: print 18 >>> for instr in dis.get_instructions(code): 19 ... print(instr) 20 … 21
«Дизассемблер» можно использовать для оценки быстродействия (стоит помнить, что инструкции имеют различное время выполнение, особенно — связанные созданием пространств имён)
Байт-код 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()
А можно перебить и code.compile_command() — получится другой язык программирования, например всё тот же Hy
«Цветной» REPL (на Python) на 2025-11-15 находится в стадии бурной разработки, у него нет устоявшегося API
…он называется _pyrepl, patches are welcome!
…
Д/З
- Прочитать:
(необязательно) dis
TODO Задача на AST
TODO Задача на inspect сигнатуру
TODO (если получится, то задача на байт-код, но скорее всего нет, тогда на стек вызовов)
- Это «обязательная задача» из бакалаврского курса на MROC3:
EJudge: WhatWhereWho 'Что? Где? Когда?'
Input:Викторина проводится по следующим правилам. Вначале участник опрашивает в заданном порядке некоторых других участников, нет ли у них ответа, причём удовлетворяется первым же вариантом. Если ответа не нашлось, он может придумать свой или признаться, что не знает. Если ответ получен, он может его скорректировать (потому что нашёл недочёт) или ответить как есть. Назовём опросным планом индивидуальный список каждого участника, по которому он опрашивает остальных. Очевидно, не всякая совокупность планов хороша:
- например, участники могут начать спрашивать друг друга по кругу;
- или будет выбран первый из вариантов ответа вместо скорректированного (который идёт дальше в плане).
Впрочем,
если несколько участников, не спрашивая друг у друга, придумали или скорректировали ответ, годится любой из вариантов;
если кто-то из участников мог бы скорректировать ответ, но его спрашивать и не собирались, это тоже нормально: мало ли, отчего ему не доверяют.
Можно ли, не противореча индивидуальным опросным планам, составить полный опросный план игрока — строгую последовательность, в которой опрашиваются участники, если вопрос задан конкретному игроку?
Построчно в виде «Кто_спрашивает: Кого_спрашивает_1, Кого_спрашивает_2, …» вводится список участников и их опросных планов. Если участник считает, что он и так всё знает, план может быть пустой. Последняя строка ввода — пустая. Запятых и двоеточий в именах нет, пробелы могут встречаться только внутри и только поштучно.
Выводится строка вида «Кого_спросили: У_кого_узнать_1, У_кого_узнать_2, …» — полный опросный план для каждого из участников в порядке их ввода
- Если полный опросный план для какого-то игрока невозможен, ни один из планов не выводится, а вместо этого выводится":
«CYCLE», если участники могут начать спрашивать друг друга по кругу
«UNKNOWN», если у кого-то в плане опроса встречается неизвестный участник
«INEFFECTIVE», если кто-то может дать нескорректированный ответ, хотя мог бы узнать скорректированный
Output:Милован Ильясович Михеев: Савватий Эдгардович Моисеев, Михалыч Савватий Эдгардович Моисеев: левый какой-то: Михалыч: Капитон Силин Капитон Силин:
Милован Ильясович Михеев: Савватий Эдгардович Моисеев, Михалыч, Капитон Силин Савватий Эдгардович Моисеев: левый какой-то: Михалыч: Капитон Силин Капитон Силин:
