我正在尝试自学python字节码是如何工作的,所以我可以通过操作函数的代码来做一些事情(只是为了好玩而不是实际使用)所以我从一些简单的例子开始,例如:
def f(x):
return x + 3/x
字节码是*:
(124, 0, 0, 100, 1, 0, 124, 0, 0, 20, 23, 83)
因此,124
是LOAD_FAST
字节码,并且正在加载的对象的名称是f.__code__.co_varnames[0]
,0
是124
之后的数字。 1}}。 100
表示要LOAD_CONST
加载f.__code__.co_consts[1]
,其中1
是100
之后的数字。但是有一堆辅助零,比如第二个,第三个和第五个零,似乎没有任何意义,至少对我而言。它们表示什么?
文本字节码:
>>> dis.dis(f)
2 0 LOAD_FAST 0 (x)
3 LOAD_CONST 1 (3)
6 LOAD_FAST 0 (x)
9 BINARY_DIVIDE
10 BINARY_ADD
11 RETURN_VALUE
*注意:在Python 3中(字节码可能与上面不同),可以通过以下方式找到字节码:
>>> list(f.__code__.co_code)
[124, 0, 100, 1, 124, 0, 27, 0, 23, 0, 83, 0]
答案 0 :(得分:12)
大量字节码接受参数(任何字节码的代码点都在dis.HAVE_ARGUMENT
或以上。那些具有2字节参数的字节码,以小端顺序。
您可以看到Python当前使用的字节码的定义及其在dis
module documenation中的含义。
使用2个字节,您可以为任何字节码提供0到65535之间的参数值,对于字节码而不是更多,您可以前缀使用EXTENDED_ARG
bytecode的字节码,再添加2个字节理论上你可以多次使用EXTENDED_ARG
,但CPython解释器使用int
作为oparg
变量,因此实际用途仅限于4字节值
从Python 3.4开始,dis
模块为您提供Instruction
instances,使您可以更轻松地内省每个字节码及其参数。使用此功能,我们可以浏览您为函数f
找到的字节代码:
>>> def f(x):
... return x + 3/x
...
>>> f.__code__.co_varnames
('x',)
>>> f.__code__.co_consts
(None, 3)
>>> import dis
>>> instructions = dis.get_instructions(f)
>>> instructions
<generator object _get_instructions_bytes at 0x10be77048>
>>> instruction = next(instructions)
>>> instruction
Instruction(opname='LOAD_FAST', opcode=124, arg=0, argval='x', argrepr='x', offset=0, starts_line=2, is_jump_target=False)
所以第一个操作码,124或LOAD_FAST
将第一个本地名称的值放在堆栈上;这是0 0
参数,little-endian解释为整数0
,是代码本地数组的索引。 dis
已填写argval
属性,向我们显示第一个本地名称为x
。在上面的会话中,我将展示如何内省代码对象以查看名称列表。
>>> instruction = next(instructions)
>>> instruction
Instruction(opname='LOAD_CONST', opcode=100, arg=1, argval=3, argrepr='3', offset=3, starts_line=None, is_jump_target=False)
下一条指令将常量推入堆栈;参数现在是1 0
,或者是整数1
的小尾数;与代码对象关联的第二个常量。 f.__code__.co_consts
元组显示它是3
,但Instruction
对象也会将其作为argval
属性。
>>> next(instructions)
Instruction(opname='LOAD_FAST', opcode=124, arg=0, argval='x', argrepr='x', offset=6, starts_line=None, is_jump_target=False)
接下来我们有另一个LOAD_FAST
,将另一个对本地名称x
的引用推送到堆栈上。
>>> next(instructions)
Instruction(opname='BINARY_TRUE_DIVIDE', opcode=27, arg=None, argval=None, argrepr='', offset=9, starts_line=None, is_jump_target=False)
这是一个没有参数的字节码 ,操作码27低于dis.HAVE_ARGUMENT
。不需要参数,因为此操作码获取堆栈中的前两个值,将它们分开,将浮点结果推回堆栈。因此,最后x
和3
常量被采用,划分并且结果会被推回。
>>> next(instructions)
Instruction(opname='BINARY_ADD', opcode=23, arg=None, argval=None, argrepr='', offset=10, starts_line=None, is_jump_target=False)
另一个无参数字节码;这个将前两个堆栈值相加,取而代之的是结果。取BINARY_TRUE_DIVIDE
的结果,并首先推送x
的值,然后将结果放回堆栈。
>>> next(instructions)
Instruction(opname='RETURN_VALUE', opcode=83, arg=None, argval=None, argrepr='', offset=11, starts_line=None, is_jump_target=False)
最后一条指令,另一条不带参数的指令。 RETURN_VALUE
结束当前帧,将结果中的最高值作为结果返回给调用者。
答案 1 :(得分:6)
在CPython 3.6之前,CPython字节码参数占用2个字节。额外的零是参数的高字节。