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()\n'], 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()\n'], 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解释器的内容,更多细节待补充。

python 解析字节码的相关方法的更多相关文章

  1. 《python解释器源码剖析》第8章--python的字节码与pyc文件

    8.0 序 我们日常会写各种各样的python脚本,在运行的时候只需要输入python xxx.py程序就执行了.那么问题就来了,一个py文件是如何被python变成一系列的机器指令并执行的呢? 8. ...

  2. python 的 字节码 导入使用

    1. python 模块文件可以通过编译为字节码的形式: 名字:model.py x = def funt(): import model print(model.x) x = "zhang ...

  3. python查看字节码

    查看字节码可以帮助我们更好的理解python的执行流程 查看字节码列表 import opcode for op in range(len(opcode.opname)): print('0x%.2X ...

  4. 从底层入手,解析字节码增强和Btrace应用

    这篇文章聊下字节码和相关的应用. 1.机器码和字节码 机器码(machine code),学名机器语言指令,有时也被称为原生码(Native Code),是电脑的CPU可直接解读的数据. 通常意义上来 ...

  5. Python 字节码是什么

    了解 Python 字节码是什么,Python 如何使用它来执行你的代码,以及知道它是如何帮到你的. 如果你曾经编写过 Python,或者只是使用过 Python,你或许经常会看到 Python 源代 ...

  6. Python字节码介绍

    了解 Python 字节码是什么,Python 如何使用它来执行你的代码,以及知道它是如何帮到你的.如果你曾经编写过 Python,或者只是使用过 Python,你或许经常会看到 Python 源代码 ...

  7. python 字节码死磕

    前言:  如果你跟我一样,对python的字节码感兴趣,想了解python的代码在内存中到底是怎么去运行的,那么你可以继续往下看,如果你是python新手,我建议你移步它处,本文适合有点基础的pyth ...

  8. Python字节码与解释器学习

    参考:http://blog.jobbole.com/55327/ http://blog.jobbole.com/56300/ http://blog.jobbole.com/56761/ 1. 在 ...

  9. 从一道CTF题学习python字节码到源码逆向

    概述: 该题来源为2022爱春秋冬季赛ezpython,难度不是很大刚好适合我这样的萌新入门 题目: 3 0 LOAD_CONST 1 (204) 3 LOAD_CONST 2 (141) 6 LOA ...

  10. 深入理解 python 虚拟机:字节码教程(1)——原来装饰器是这样实现的

    深入理解 python 虚拟机:字节码教程(1)--原来装饰器是这样实现的 在本篇文章当中主要给大家介绍在 cpython 当中一些比较常见的字节码,从根本上理解 python 程序的执行.在本文当中 ...

随机推荐

  1. Codeforces Round #703 (Div. 2) A、B、D、E题解

    写在前边 链接:Codeforces Round #703 (Div. 2) 这次的交互题是真不会做. A. Shifting Stacks 链接:A题链接 题目大意: 有\(n\)摞高度分别为\(h ...

  2. adb从基础到进阶

    一.adb的工作原理 adb是cs架构,由三部分组成,分别是client,server,daemon,他们的关系见下图 server是整个架构的核心 server负责接收client的指令,然后将指令 ...

  3. python内置模块——logging

    内置模块-logging loging模块是python提供的内置模块,用来做日志处理. 日志等级: 等级 释义 级别数值 CRITICAL(fatal) 致命错误,程序根本跑不起来 50 ERROR ...

  4. Object.assign () 和深拷贝

    先看看啥叫深拷贝?啥叫浅拷贝? 假设B复制了A,修改A的时候,看B是否发生变化: 如果B跟着也变了,说明是浅拷贝,拿人手短!(修改堆内存中的同一个值) 如果B没有改变,说明是深拷贝,自食其力!(修改堆 ...

  5. [ARC122E] Increasing LCMs

    Problem Statement We have a sequence of $N$ positive integers: $A_1,A_2,\cdots,A_N$. You are to rear ...

  6. [ABC248G] GCD cost on the tree

    Problem Statement You are given an undirected tree with $N$ vertices. Let us call the vertices Verte ...

  7. 从根上理解elasticsearch(lucene)查询原理(2)-lucene常见查询类型原理分析

    大家好,我是蓝胖子,在上一节我提到要想彻底搞懂elasticsearch 慢查询的原因,必须搞懂lucene的查询原理,所以在上一节我分析了lucene查询的整体流程,除此以外,还必须要搞懂各种查询类 ...

  8. MySQL日期查询

    MySQL日期查询 1.今天 select * from 表名 where to_days(时间字段名) = to_days(now()); 2.昨天 SELECT * FROM 表名 WHERE T ...

  9. Scrapyd、scrapyd-client部署爬虫项目

    命令参考:https://github.com/scrapy/scrapyd-client https://scrapyd.readthedocs.io 安装组件 pip install scrapy ...

  10. ElasticSearch之禁用交换分区

    操作系统将进程加载至内存中执行时,对于当前未使用到的内存页,可能会将相关内存页交换至硬盘上,即swap. 对于性能敏感.时延敏感的应用程序比如ElasticSearch,swap特性会明显影响性能和稳 ...