Python对不可变序列进行重复拼接操作效率会很低,因为每次都会生成一个新的对象,解释器需要把原来对象中的元素先复制到新的对象里,然后再追加新的元素。
但是CPython对字符串操作进行了优化,因为对字符串做+=操作实在是太普遍了。因此,初始化str时会预留出额外的可扩展空间,从而进行增量操作的时候不会有复制再追加的这个步骤。
通过字节码研究一下这个过程。
1
2
3
4
5
6
7
8
|
>>> s_code = 'a += "b"'
>>> c = compile (s_code, ' ', ' exec ')
>>> c.co_code
b 'e\x00\x00d\x00\x007Z\x00\x00d\x01\x00S'
>>> c.co_names
( 'a' ,)
>>> c.co_consts
( 'b' , None )
|
得到的字节码是Bytes类型的。这里穿插一些Bytes类型的知识。
Bytes类型
b'e\x00\x00d\x00\x007Z\x00\x00d\x01\x00S',b表示是Bytes类型。Bytes以二进制字节序列的形式记录数据,每一个字符就代表一个字节(8位)。比如上面的e表示二进制0110 0101。部分ASCII码对照表如下图所示。
但是,不是所有的字节都是可显示的,甚至有些字节无法对应到ASCII码上(因为ASCII码只定义了128个字符,而一个字节有256个)。比如0000 0000对应的ASCII是不可显示的、0111 1111没有对应的ASCII码。
为了表示这些无法显示的字节,就引入了\x符号,其表示后续的字符为16进制。如,\x00表示16进制的00,也就是二进制的0000 0000。
至此,所有字节都可被表示。
字节码分析
回到开始的代码。为了显示方便,将b'e\x00\x00d\x00\x007Z\x00\x00d\x01\x00S'转为16进制来显示。
1
2
|
>>> c.co_code. hex ()
'650000640000375a000064010053'
|
通过opcode.opname函数可以得到操作码所对应的操作指令
1
2
3
|
>>> import opcode
>>> opcode.opname[ 0x65 ]
'LOAD_NAME'
|
因此,完整的字节码可以解释为(TOS即top-of-stack,栈顶元素):
1
2
3
4
5
6
7
8
9
10
11
|
字节:位置,功能
65 : 0 ,LOAD_NAME
0000 :参数,将co_names[ 0 ]的值,即a的值,压入栈
64 : 3 ,LOAD_CONST
0000 :参数,将co_consts[ 0 ],即 'b' ,压入栈
37 : 6 ,INPLACE_ADD,TOS = TOS1 + TOS
5a : 7 ,STORE_NAME
0000 :参数,co_names[ 0 ] = TOS,即将栈顶赋值给a
64 : 10 ,LOAD_CONST
0100 :参数
53 : 13 ,RETURN_VALUE,Returns with TOS to the caller of the function
|
实际上借助dis函数可以直接获得可读的字节码:
1
2
3
4
5
6
7
8
|
>>> import dis
>>> dis.dis(s_code)
1 0 LOAD_NAME 0 (a)
3 LOAD_CONST 0 ( 'b' )
6 INPLACE_ADD
7 STORE_NAME 0 (a)
10 LOAD_CONST 1 ( None )
13 RETURN_VALUE
|
完整代码:
1
2
3
4
5
6
7
8
|
s_code = 'a += "b"'
c = compile (s_code, ' ', ' exec ')
c.co_code
c.co_names
c.co_consts
c.co_code. hex ()
import dis
dis.dis(s_code)
|
非常失败,对比了string和tuple的赋值字节码,并没有看出string的优化…
以上就是本次关于python字节码的相关知识点,感谢你对服务器之家的支持。
原文链接:http://blog.csdn.net/Titan0427/article/details/79263760