• python 解析字节码的相关方法


    python代码被解释器执行时分为两步走:
    一、python编译器将代码编译成字节码
    二、python虚拟机执行字节码
    由于这两步是一起的,所以在python编程中很少能看到字节码。但是想要提高代码效率就必须知道一段python源码对应的字节码是怎么样的,拨开迷雾看本质。本文分析python常见的能编译成字节码相关的函数。

    compile

    compile() 函数是python的内置函数,功能是将python源代码编译为code对象或AST对象。

    函数定义:

    compile(source, filename, mode[, flags[, dont_inherit]])
    

    参数说明

    • source:字符串或AST(Abstract Syntax Trees)对象,表示需要进行编译的Python代码
    • filename:指定需要编译的代码文件名称,如果不是从文件读取代码则传递一些可辨认的值(通常是用'')
    • mode:用于标识必须当做那类代码来编译,规则如下:
      如果source是由一个代码语句序列组成,则指定mode='exec';
      如果source是由单个表达式组成,则指定mode='eval';
      如果source是由一个单独的交互式语句组成,则指定mode='single'。
    • 另外两个可选参数暂不做介绍

    简单例子

    ss = """
    a = 100
    b = 200
    c = a + b
    print(c)
    """
    
    co = compile(ss, 'string', 'exec')
    print(co)
    print(dir(co))
    exec(ss)
    
    <code object <module> at 0x7f51c25e8810, file "string", line 2>
    ['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 'co_kwonlyargcount', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_stacksize', 'co_varnames']
    300
    

    变量co就是一个字节码对象,通过dir(co)能够看到该字节码对象中有很多属性。code对象是一个对象,在CPyhon解释器中,该对象就是一个结构体,结构体如下:

    typedef struct {
        PyObject_HEAD		/* 头部信息, 我们看到真的一切皆对象, 字节码也是个对象 */	
        int co_argcount;            /* 可以通过位置参数传递的参数个数 */
        int co_posonlyargcount;     /* 只能通过位置参数传递的参数个数,  Python3.8新增 */
        int co_kwonlyargcount;      /* 只能通过关键字参数传递的参数个数 */
        int co_nlocals;             /* 代码块中局部变量的个数,也包括参数 */
        int co_stacksize;           /* 执行该段代码块需要的栈空间 */
        int co_flags;               /* 参数类型标识 */
        int co_firstlineno;         /* 代码块在对应文件的行号 */
        PyObject *co_code;          /* 指令集, 也就是字节码, 它是一个bytes对象 */
        PyObject *co_consts;        /* 常量池, 一个元组,保存代码块中的所有常量。 */
        PyObject *co_names;         /* 一个元组,保存代码块中引用的其它作用域的变量 */
        PyObject *co_varnames;      /* 一个元组,保存当前作用域中的变量 */
        PyObject *co_freevars;      /* 内层函数引用的外层函数的作用域中的变量 */
        PyObject *co_cellvars;      /* 外层函数中作用域中被内层函数引用的变量,本质上和co_freevars是一样的 */
     
        Py_ssize_t *co_cell2arg;    /* 无需关注 */
        PyObject *co_filename;      /* 代码块所在的文件名 */
        PyObject *co_name;          /* 代码块的名字,通常是函数名或者类名 */
        PyObject *co_lnotab;        /* 字节码指令与python源代码的行号之间的对应关系,以PyByteObject的形式存在 */
        
        //剩下的无需关注了
        void *co_zombieframe;       /* for optimization only (see frameobject.c) */
        PyObject *co_weakreflist;   /* to support weakrefs to code objects */
        void *co_extra;
        unsigned char *co_opcache_map;
        _PyOpcache *co_opcache;
        int co_opcache_flag; 
        unsigned char co_opcache_size; 
    } PyCodeObject;
    

    可以看到该结构提中的有很多成员变量,这些成员变量也反应在co的变量和方法中。比较重要的有:

    1. co_code 字节码
    2. co_consts 常量池
    3. co_names 符号池

    到这里我们能看到的还是code对象,并没有看到具体的字节码,co_code 是指向具体的字节码,但是由于是2进制,不便于查看。所有如果想看到纯粹的字节码,我们可以用下面的dis模块。

    dis

    dis是python的标准库模块,功能是将一段python代码编译成字节码指令。

    python代码的执行过程分为两步:1.python解释器将代码编译成字节码;2.python虚拟机执行字节码。通常这两个步骤是一次性完成,所以我们看不到中间状态的字节码。而dis就可以将一段源码编译成字节码。

    从上面的code对象中可以到看字节码是其一个成员变量co_code,字节码是被底层结构体PyCodeObject的成员co_code指向,那么dis可以取出字节码指令。

    简单例子

    import dis
    
    ss = """
    a = 100
    b = 200
    c = a + b
    print(c)
    """
    
    
    byte_str = dis.dis(ss)
    
    

    ss这一段python代码的字节码就如下:

      2           0 LOAD_CONST               0 (100)
                  2 STORE_NAME               0 (a)
    
      3           4 LOAD_CONST               1 (200)
                  6 STORE_NAME               1 (b)
    
      4           8 LOAD_NAME                0 (a)
                 10 LOAD_NAME                1 (b)
                 12 BINARY_ADD
                 14 STORE_NAME               2 (c)
    
      5          16 LOAD_NAME                3 (print)
                 18 LOAD_NAME                2 (c)
                 20 CALL_FUNCTION            1
                 22 POP_TOP
                 24 LOAD_CONST               2 (None)
                 26 RETURN_VALUE
    

    首先解释一下,python编译器编译过一段代码之后,生成的是code对象,对象就是compile中看到的结构体。这个对象中有字节码信息co_code,也有一些变量的信息,就是 co_consts 常量池 和 co_names 符号池。那么把这两个成员也打印出来。

    (100, 200, None)
    ('a', 'b', 'c', 'print')
    

    下面解释一下字节码。这段字节码就是python源码被编译之后的样子,也是最终被执行的对象。

    2             0 LOAD_CONST               0 (100)
                  2 STORE_NAME               0 (a)
    

    2 代表字节码对应的源码的行号
    0 LOAD_CONST 代表字节码的行号和字节码指令
    0 (100) 代表字节码的操作数

    完整的说

    2             0 LOAD_CONST               0 (100)
    

    意思是:从常量池加载一个常量,加载的常量是序号为0,值为100

    2 STORE_NAME               0 (a)
    

    意思是:从符号池取一个符号,符号的序号是0,值为a,绑定到上一个操作的常量100。然后将a=100,存入命名空间中。

    3           4 LOAD_CONST               1 (200)
                6 STORE_NAME               1 (b)
    

    和上面的类似。从常量池中取下标为1的常量,值为200,然后去符号池去下标为1的符号为b,绑定b=200,然后存入到命名空间中。

                  8 LOAD_NAME                0 (a)
                 10 LOAD_NAME                1 (b)
                 12 BINARY_ADD
                 14 STORE_NAME               2 (c)
    
    8 LOAD_NAME                0 (a)
    

    加载一个符号对应的值,符号是a,所以加载的值是100,

    10 LOAD_NAME                1 (b)
    

    加载符号b对应的值,值为200

    12 BINARY_ADD
    

    加法运算

    14 STORE_NAME               2 (c)
    

    从符号池去下标为2的元素,为符号c,然后绑定到上一步的结果300,然后存入到命名空间中。

    inspect

    inspect是python标准库模块,inspect模块四大功能:
    1、类型检查(type checking)
    2、获取源码(getting source code)
    3、获取类和方法的参数信息(inspecting classes and functions)
    4、解析堆栈(examining the interpreter stack)

    其中对于分析代码字节码重要的包括:

    inspect.stack() 获取调用者当前的堆栈信息
    inspect.currentframe() 获取调用者当前Frame对象信息

    简单例子

    import inspect
    
    
    def fun_demo():
        a = 100
        b = 200
        c = a + b
    
        s_ob = inspect.stack()
        print(s_ob)
    
        f_ob = inspect.currentframe()
        print(f_ob)
        print(dir(f_ob))
    
       
    fun_demo()
    
    [FrameInfo(frame=<frame at 0x7f9e884e4048, file 'inspect_demo.py', line 10, code fun_demo>, filename='inspect_demo.py', lineno=9, function='fun_demo', code_context=['    s_ob = inspect.stack()
    '], index=0), FrameInfo(frame=<frame at 0x7f9e8861a9f8, file 'inspect_demo.py', line 18, code <module>>, filename='inspect_demo.py', lineno=18, function='<module>', code_context=['fun_demo()
    '], index=0)]
    <frame at 0x7f9e884e4048, file 'inspect_demo.py', line 13, code fun_demo>
    ['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'clear', 'f_back', 'f_builtins', 'f_code', 'f_globals', 'f_lasti', 'f_lineno', 'f_locals', 'f_trace', 'f_trace_lines', 'f_trace_opcodes']
    

    Frame对象是python真正执行的对象,字节码对象code对象属于Frame对象
    我们都知道python解释器是模拟了真实的机器,所以需要有堆栈信息,而Frame就是python代码执行的堆栈信息的体现。其在Cpython中是一个结构体,如下:

    typedef struct _frame {
        PyObject_VAR_HEAD  		/* 可变对象的头部信息 */
        struct _frame *f_back;      /* 上一级栈帧, 也就是调用者的栈帧 */
        PyCodeObject *f_code;       /* PyCodeObject对象, 通过栈帧对象的f_code可以获取对应的PyCodeObject对象 */
        PyObject *f_builtins;       /* builtin命名空间,一个PyDictObject对象 */
        PyObject *f_globals;        /* global命名空间,一个PyDictObject对象 */
        PyObject *f_locals;         /* local命名空间,一个PyDictObject对象  */
        PyObject **f_valuestack;    /* 运行时的栈底位置 */
    
        PyObject **f_stacktop;      /* 运行时的栈顶位置 */
        PyObject *f_trace;          /* 回溯函数,打印异常栈 */
        char f_trace_lines;         /* 是否触发每一行的回溯事件 */
        char f_trace_opcodes;       /* 是否触发每一个操作码的回溯事件 */
    
        PyObject *f_gen;            /* 是否是生成器 */
    
        int f_lasti;                /* 上一条指令在f_code中的偏移量 */
    
        int f_lineno;               /* 当前字节码对应的源代码行 */
        int f_iblock;               /* 当前指令在栈f_blockstack中的索引 */
        char f_executing;           /* 当前栈帧是否仍在执行 */
        PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* 用于try和loop代码块 */
        PyObject *f_localsplus[1];  /* 动态内存,维护局部变量+cell对象集合+free对象集合+运行时栈所需要的空间 */
    } PyFrameObject;
    

    其中 *f_code 就指向的是code对象,除此之外还有

    1. *f_builtins 代码执行的内建命名空间
    2. *f_globals 代码执行的全局命名空间
    3. *f_locals 代码执行的局部命名空间

    小结

    通过这三个模块来剖析额字节码,涉及到很多python解释器的内容,更多细节待补充。

  • 相关阅读:
    天天写业务代码,如何成为技术大..niu?
    2021年12月PHP面试题总结
    同事乱用 Redis 卡爆,我真是醉了...
    如何合理的面试审问面试官:
    进制转换
    Is there a difference between table.class tr, th and table .class tr, th?
    ubuntu中安装notepadqq
    VPDN
    OpenAppFilter 自定义特征库
    Tcping详细使用教程
  • 原文地址:https://www.cnblogs.com/goldsunshine/p/15049341.html
Copyright © 2020-2023  润新知