社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

字节码:分析 Python 执行的终极利器

Python中文社区 • 3 年前 • 391 次点击  

一、什么是代码对象

Code Object(代码对象)封装了 Python 虚拟机的字节码和虚拟机执行相关信息,可以把字节码称为 Python 虚拟机上的汇编语言。
学代码对象有什么用呢?从其定义可知,字节码是编译后的 Python 代码,学习代码对象有助于我们理解 Python 虚拟机、编译过程、执行过程,更加深刻理解 Python 语言特性和疑难点。在解决一些疑难杂症时,查看代码对象的字节码往往有事半功倍的效果。

二、探索代码对象

Python 程序由代码块组成,代码块可以是模块、函数或类,也可以是脚本文件,还可以是python - c 'string'exec ('string')eval ('string')中字符串的内容。

两种方法:

  • fun.__code__ 获取函数 fun 主体的代码对象

  • compile('source code','','exec')获取代码块 source code 的代码对象

三、一个函数及其代码对象

我们定义一个函数fun(a,b),它完成简单的加法运算。
def fun(a,b):
    return a+b 
来看它的代码对象属性情况,我们重点关注以 co_ 开头的属性。
for attr in dir(fun.fun.__code__):
    if attr.startswith('co_'):
        print("{attr}:\t{attrs}".format(
            attrs=getattr(attr=attr,fun.fun.__code__, attr)))
输出为:
co_argcount: 2
co_cellvars: ()
co_code: b'|\x00|\x01\x17\x00S\x00'
co_consts: (None,)
co_filename: G:\pythonCodeStudy\manuscript\0 bytecode\fun.py
co_firstlineno: 1
co_flags: 67
co_freevars: ()
co_kwonlyargcount: 0
co_lnotab: b'\x00\x01'
co_name: fun
co_names: ()
co_nlocals: 2
co_posonlyargcount: 0
co_stacksize: 2
co_varnames: ('a''b')

fun 函数主体的代码对象的属性意义如下:
  • co_argcount 函数形式参数个数,这个只有函数类型代码块的代码对象有,其它类型代码块没有该属性。
  • co_code 字节码指令序列,字节码都由操作码 opcode 和参数 opatg 组成的序列。
  • co_const 常量列表,列表内容包括如下:
    • None 函数返回值,系统自带。
    • 从前往后数所有字面常量:数字和字符串。
    • 内嵌函数代码对象 code object。
    • 内嵌函数的 qual_name 常量,如:outer..inner。
  • co_name 本函数的名字。
  • co_varnames 本函数 局部变量 ,不含被引用自由变量,包含形式参数和内嵌函数名。
  • co_names 本函数用到的非局部变量,也就是 全局变量、系统内置变量
  • co_nlocals 本函数的局部变量个数。
  • co_cellvars cell 变量。
  • co_freevars 自由变量。
  • co_flag 代码对象的种类,比如协程、生成器等,其意义定义在 include/code.h 中。
  • co_lnotab 计算字节码偏移量代表的源代码行号的字节序列。两个字节序列为一个单位,指示两条源代码指令编译成的字节码之间偏移多少。
  • co_stacksize 执行字节码指令时,计算栈上最大的项目数,和函数参数个数有关。
Python虚拟机是基于栈的机器,每步函数调用产生栈帧(stack frame)。每个栈帧包含计算栈和块栈。所有参数压入计算栈,调用时弹栈,计算后弹出结果,结束本栈帧。
上例中,我们没有详细解释 co_cellvars 和 co_freevars,下面详细解释。

3.1 co_cellvars 和 co_freevars

co_cellvars 和 co_freevars 是一个相对的概念。我们写一个嵌套函数来解释这两个概念:函数 outer 中,定义了一个变量 e,被内部嵌套函数 inner 引用;inner 函数内部使用了变量 e,但是并未在该块内定义之。
def outer(o1, o2='o2'):
    e = 'enclose'

    def inner(i1, i2='i2'):
        print(e)
        return e
    return inner
print(outer.__code__.co_cellvars)
print(outer('i1').__code__.co_freevars)
结果。
('e',)
('e',)
故此,我们知道:
  • co_cellvars 是被内部嵌套块(函数)引用的变量组成的元组。外部函数创建特殊的 cell 对象存储该变量,cell 对象的生存周期超过了定义它的外部函数。这句话理解就是:外部变量执行完之后,清理现场,它的变量都消失了,但 cell 对象不消失,仍然存在。这也就是所谓 * cell 变量*

  • co_freevars 就块内使用,但是并未在该块内定义的变量(不含全局变量、内置变量)。也就是当前块(函数) 引用的外部 cell 变量 组成的元组,和上个 co_cellvars 是相对的概念

这两个是一体两面的变量。
  • 外部变量 outer 作用域里,创建了变量 e 以及变量 inner (函数)。

  • 因为嵌套函数 inner 使用了外部变量 e ,所以在 outer 函数里, e 是作为 cell 变量,绑定到特殊的 cell 对象里。

  • 这个特殊的 cell 对象和 inner 绑定在了一起,这样 outer 作用域消失的时候, inner 内部借由 cell 对象访问到了 e 这个变量,它对 inner 函数来说是(来自 cell 对象的)自由变量。

掌握自由变量的概念,是编写带状态函数、装饰器的基础。

四、字节码细节

字节码看起来就像乱码,如上文 fun 函数的字节码:co_code : b '|\ x00 |\ x01 \ x17 \ x00S \ x00 '
根据上文,字节码由一位操作码和一位参数组成的序列。让我们分析细节。
co_code [0]表示第一个操作码 |,这是 ASCII 码 124 表示的字符,在 include/opcode.h 中,可以看到 124 是 LOAD_FAST 操作码,这是对局部变量列表进行的加载操作。其它类似的:比如 LOAD_CONST 就是对字面常量列表操作,LOAD_GLOBAL 是对全局变量操作。
如果操作码不带参数,参数可以省略。这里的第一个操作码的参数是 co_code[1] 为 0x00 。
因此,这个完整的字节码操作是把局部变量列表 co_varnames 的第 0x00 索引内容 a ,压入计算栈栈顶。
字节序列看起来比较费劲,让我们用 dis.dis(fun)来反汇编代码,得到字节码如下。
  2           0 LOAD_FAST                0 (a)
              2 LOAD_FAST                1 (b)
              4 BINARY_ADD
              6 RETURN_VALUE
这样看起来就很简单了。
  • 第一列 2 表示源代码的第二行,也就是return a+b这一条。
  • 第二列的 0 表示该条操作码相对字节码开头的偏移量,LOAD_FAST 表示操作码,0 表示参数,(a)是由 dis 生成的,即局部变量元组第 0 个元素 a。本条将变量 a 压入计算栈。
  • 以此类推第二条,将局部变量 b 压入计算栈。
  • 第三条 BINARY_ADD 没有参数,它是求和,将 a 和 b 弹出,求和,结果压入计算栈栈顶。
  • 第四条 RETURN_VALUE 弹出结果,结束本栈帧。
Python 编译产生 pyc 文件:Python3 之前是在本地目录产生,之后是在 pycache 目录下。我们打开上例产生的 pyc 文件,使用十六进制查看,能明显发现,编译的字节码就直接在 PYC 文件里。

五、其它代码块代码对象

上例中,我们对函数进行反汇编,使用的是 dis.dis(fun)指令,这里的 fun 是函数。
第二部分说过得到代码对象有两种方法,当我们对其它代码对象进行 compile 时,实际是对该模块进行反编译。如果此时该代码块里有函数,只会产生代码对象,不会产生真正的函数对象。
比如如下语句:print(dis.dis(compile('def fun(a,b): return a+b', '', 'exec'))),输出如下。可见此时的函数只是一个代码对象,作为常量载入,MAKE_FUNCTION 后,赋值给 fun 局部变量。
  1           0 LOAD_CONST               0 ( "", line 1>)
              2 LOAD_CONST               1 ('fun')
              4 MAKE_FUNCTION            0
              6 STORE_NAME               0 (fun)
              8 LOAD_CONST               2 (None)
             10 RETURN_VALUE

六、总结

代码对象封装了 Python 虚拟机的字节码和其它编译相关信息,可以把字节码称为 Python 虚拟机上的汇编语言。我们分析了自由变量和 cell 变量,掌握自由变量的概念,这是编写装饰器、带状态函数的基础。字节码由一位操作码和一位参数组成的序列,学习其细节,有助于我们理解 Python 的特性。可在分析变量作用域、闭包时作为强大的工具。

作者:巩庆奎,大奎,对计算机、电子信息工程感兴趣。gongqingkui at 126.com

赞 赏 作 者






点击下方阅读原文加入社区会员

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/115081
 
391 次点击