На этом шаге мы рассмотрим использование модуля dis.
Вы хотите узнать в подробностях, как работает ваш код "под капотом", путем дизассемблирования его в низкоуровневый байт-код с помощью интерпретатора.
Модуль dis можно использовать для вывода результата дизассемблирования любой функции Python. Например:
>>> def countdown(n): while n > 0: print('T-minus', n) n -= 1 print('Blastoff!') >>> import dis >>> dis.dis(countdown) 2 >> 0 LOAD_FAST 0 (n) 2 LOAD_CONST 1 (0) 4 COMPARE_OP 4 (>) 6 POP_JUMP_IF_FALSE 28 3 8 LOAD_GLOBAL 0 (print) 10 LOAD_CONST 2 ('T-minus') 12 LOAD_FAST 0 (n) 14 CALL_FUNCTION 2 16 POP_TOP 4 18 LOAD_FAST 0 (n) 20 LOAD_CONST 3 (1) 22 INPLACE_SUBTRACT 24 STORE_FAST 0 (n) 26 JUMP_ABSOLUTE 0 5 >> 28 LOAD_GLOBAL 0 (print) 30 LOAD_CONST 4 ('Blastoff!') 32 CALL_FUNCTION 1 34 POP_TOP 36 LOAD_CONST 0 (None) 38 RETURN_VALUE >>>
Модуль dis может быть полезен, если вам когда-либо потребуется изучить, что происходит в ваших программах на очень низком уровне (например, если вы пытаетесь разобраться в характеристиках производительности).
Сырой байт-код, интепретируемый функцией dis(), доступен в функциях:
>>> countdown.__code__.co_code
b'|\x00d\x01k\x04r\x1ct\x00d\x02|\x00\x83\x02\x01\x00|\x00d\x038\x00}
\x00q\x00t\x00d\x04\x83\x01\x01\x00d\x00S\x00'
Если вы захотите интерпретировать этот код самостоятельно, вам понадобятся некоторые из констант, определенных в модуле opcode. Например:
>>> c = countdown.__code__.co_code >>> import opcode >>> opcode.opname[c[0]] 'LOAD_FAST' >>> opcode.opname[c[3]] 'POP_TOP' >>> opcode.opname[c[2]] 'LOAD_CONST' >>>
Иронично, но в модуле dis нет функции, которая облегчила бы вам программную обработку байт-кода. Однако эта функция-генератор примет сырую последовательность байт-кода и превратит ее в опкоды (коды операций) и аргументы:
>>> import opcode >>> def generate_opcodes(codebytes): extended_arg = 0 i = 0 n = len(codebytes) while i < n: op = codebytes[i] i += 1 if op >= opcode.HAVE_ARGUMENT: oparg = codebytes[i] + codebytes[i+1]*256 + extended_arg extended_arg = 0 i += 2 if op == opcode.EXTENDED_ARG: extended_arg = oparg * 65536 continue else: oparg = None yield (op, oparg) >>>
Чтобы использовать эту функцию, напишите такой код:
>>> for op, oparg in generate_opcodes(countdown.__code__.co_code): print(op, opcode.opname[op], oparg) 124 LOAD_FAST 25600 1 POP_TOP None 107 COMPARE_OP 29188 28 INPLACE_FLOOR_DIVIDE None 116 LOAD_GLOBAL 25600 2 ROT_TWO None 124 LOAD_FAST 33536 2 ROT_TWO None 1 POP_TOP None 0 <0> None 124 LOAD_FAST 25600 3 ROT_THREE None 56 INPLACE_SUBTRACT None 0 <0> None 125 STORE_FAST 28928 0 <0> None 116 LOAD_GLOBAL 25600 4 DUP_TOP None 131 CALL_FUNCTION 257 0 <0> None 100 LOAD_CONST 21248 0 <0> None >>>
Это малоизвестный факт, но вы можете заменить сырой байт-код любой функции, какой пожелаете. Это требует некоторых усилий, но вот пример того, как это работает:
>>> def add(x, y): return x + y >>> c = add.__code__ >>> c <code object add at 0x0000026EFA8759D0, file "<pyshell#30>", line 1> >>> c.co_code b'|\x00|\x01\x17\x00S\x00' >>> >>> # Создаем полностью новый объект кода с фейковым байт-кодом >>> import types >>> newbytecode = b'xxxxxxx' >>> nc = types.CodeType(c.co_argcount, c.co_kwonlyargcount, c.co_nlocals, c.co_stacksize, c.co_flags, newbytecode, c.co_consts, c.co_names, c.co_varnames, c.co_filename, c.co_name, c.co_firstlineno, c.co_lnotab) >>> nc <code object add at 0x10069fe40, file "<stdin>", line 1> >>> add.__code__ = nc >>> add(2, 3) Segmentation fault
Сбой интрепретатора - это самый вероятный исход таких безумных экспериментов. Однако разработчики, выполняющие продвинутую оптимизацию и создание инструментов метапрограммирования, могут заниматься этим в реальной жизни. Эта последняя часть иллюстрирует то, как это можно сделать.
Со следующего раза мы будем рассматривать модули и пакеты.