Интроспекция и байткод
Интроспекция — возможность запросить тип и структуру объекта во время выполнения программы.
В 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__', None), ('__module__', '__main__'), ('__slots__', ('a', 'b', 'c')), ('d', 100500), ('fun', <bound method C.fun of <__main__.C object at 0x7f9a3dd7ec80>>)] 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())],
34 keywords=[]))],
35 orelse=[])],
36 type_ignores=[])
Если очень грубо: типы данных, имена, 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 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'\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.
- Классическое фон-неймановское последовательное выполнение инструкций с 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 0 RESUME 0 16 17 1 2 PUSH_NULL 18 4 LOAD_NAME 0 (range) 19 6 LOAD_CONST 0 (5) 20 8 CALL 1 21 16 GET_ITER 22 >> 18 FOR_ITER 10 (to 42) 23 22 STORE_NAME 1 (i) 24 25 2 24 PUSH_NULL 26 26 LOAD_NAME 2 (print) 27 28 LOAD_NAME 1 (i) 28 30 CALL 1 29 38 POP_TOP 30 40 JUMP_BACKWARD 12 (to 18) 31 32 1 >> 42 END_FOR 33 44 RETURN_CONST 1 (None) 34
- Иное:
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.
Например, начиная с 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()
А можно перебить и code.compile_command()
…
Д/З
- Прочитать:
(необязательно) dis
TODO