Интроспекция и байткод
Интроспекция — возможность запросить тип и структуру объекта во время выполнения программы.
В 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
EJudge: PseudoQuine 'Нечестный квайн'
Написать функцию quine(), которая возвращает строку, содержащую её собственный исходный текст на Python, то есть работает как квайн. Однако эта функция квайном не является, потому что получает свой исходный текст с помощью inspect. ☺
True
EJudge: SequenceDiv 'Деление последовательностей'
Написать программу (внимание! это именно программа, а не функция или класс), входные данные для которой — это текст на ЯП Python. Этот текст следует выполнить с помощью exec(), предварительно заменив в нём операцию целочисленного деления «//» на более «продвинутую», которая применима также к конечным индексируемым последовательностям и возвращает соответствующую долю этой последовательности, начиная с нулевого элемента. Операцию «//=» реализовывать не надо.
1 print(123 * 4 // 3, "123" * 4 // 3)
164 1231
EJudge: SchMackross 'Макросы-Шмакросы'
Написать класс macro со следующими свойствами:
@macro — декоратор, который «превращает функцию в макрос» (см. пояснения далее). Получившаяся функция должна продолжать работать.
@macro() — декоратор, «раскрывающий макросы» в функции: заменяет в ней все макровызовы конструкции вида макрос(фактические параметры) на тело макроса, в котором вместо формальных параметров подставлены фактические
Для простоты соблюдаются следующие требования:
Макрос работает с исходным текстом функции посредством inspect.getsource()
Функция под декоратором @macro имеет только позиционные параметры и состоит из единственного оператора return выражение (то есть практически именованная labmda)
Вложенных макросов ни в макроопределениях, ни в макроподстановках нет (т. е. нельзя написать macro1(macro2(…))
- Вложенных декораторов (вообще других декораторов) тоже нет
- Переопределять макрос нельзя (определять несколько — можно)
27 10