Eleven's blog


  • 首页

  • 归档

  • 关于

  • 遗愿

Python字节码

发表于 2014-10-07 | 分类于 Python源码剖析

绝对清晰,是风格上唯一的美。既然是美的东西,我们总是要追求的,如果不熟悉Python虚拟机中执行的字节码,那么又怎么能被称作是绝对清晰呢?

首先是最简单的简单内建对象的创建。看下面四句语句。

[simple_obj.py]
i = 1
s = "Python"
d = {}
l = []
这个文件被编译后的对应的PyCodeObject对象的co_names和co_consts的xml结构如下
<consts>
    <int value="1" />
    <internStr index="0" length="6" value="Python" />
    <NoneObject />
</consts>
<names>
    <internStr index="1" length="1" value="i" />
    <internStr index="2" length="1" value="s" />
    <internStr index="3" length="1" value="d" />
    <internStr index="4" length="1" value="l" />
</names>
co_names存放了这段代码的所有局部变量名,co_consts存放了这段代码中的所有静态变量 Python解释器执行字节码的入口函数就是PyEval_EvalFrameEx()。这个函数中定义了大量的宏
GETITEM(v, i)   // 从tuple中获取元素
STACKADJ(n)     // 调整栈顶指针的位置
PUSH(v)         // 入栈
POP()           // 出栈
i = 1 对应的字节码如下
0 LOAD_CONST    0   (1)     // 加载偏移量为0的常量
3 STORE_NAME    0   (i)     // 将加载的值与偏移量为0的变量名形成约束,并存入local命名空间。

s = "Python" 对应的字节码如下
6 LOAD_CONST    1   ("Python")
9 STORE_NAME    1   (s)

d = {} 对应的字节码如下
12 BUILD_MAP    0
15 STORE_NAME   2   (d)

l = []
18 BUILD_LIST   0
21 STORE_NAME   3   (l)
24 LOAD_CONST   2   (none)  // 24和27的语句表示python执行完一段codeblock后必须有返回值,默认返回None。
27 RETURN_VALUE

而LOAD_CONST的实现可以当作下面的语句
x = GETITEM(consts, oparg); // 从consts中获取对应偏移量的元素值
Py_INCREF(x);               // 增加x变量的引用计数
PUSH(x);                    // 入栈

BUILD_MAP的实现如下
x = PyDict_New();
PUSH(x);

RETURN_VALUE的实现如下
retval = POP();
why = WHY_RETURN;       // why在Python虚拟机的消息循环中作一个标志,异常机制就是用它实现的
[adv_obj.py]
i = 1
s = "Python"
d = {"1": 1, "2": 2}
l = [1, 2]

adv_obj.py对应的PyCodeObject的names和consts的xml结构如下
<consts>
    <int value="1" />
    <internStr index="0" length="6" value="Python" />
    <internStr index="1" length="1" value="1" />
    <int value="2" />
    <internStr index="2" length="1" value="2" />
    <NoneObject />
</consts>
<names>
    <internStr index="3" length="1" value="i" />
    <internStr index="4" length="1" value="s" />
    <internStr index="5" length="1" value="d" />
    <internStr index="6" length="1" value="l" />
</names>
这里consts中只有一个<int value="1">是因为Python对整数的处理也有一套类似雨字符串的internal机制。

d = {"1": 1}
12 BUILD_MAP    0
15 DUP_TOP                  // 增加了map的引用计数,将map再一次压入栈中
16 LOAD_CONST   0   (1)
19 ROT_TWO                  // 将栈顶元素和第二个元素交换位置
20 LOAD_CONST   2   ("1")
23 STORE_SUBSCR             // 将("1": 1)这个item插入到上面的map中,并将栈顶指针上移三个
24 DUP_TOP
25 LOAD_CONST   3   (2)
28 ROT_TWO
29 LOAD_CONST   4   ("2")
32 STORE_SUBSCR
33 STORE_NAME   2   (d)     // 将上面的map与偏移量为2的变量名(d)生成约束,存入local空间中

[DUP_TOP]       // 注意 DUP_TOP不仅仅增加了栈顶元素的引用计数,还将栈顶元素再一次压入栈中
v = TOP();
Py_INCREF(v);
PUSH(v);

[ROT_TWO]
v = TOP()
w = SECOND();
SET_TOP(w);
SET_SECOND(v);

[STORE_SUBSCR]
w = TOP();
v = SECOND();
u = THIRD();
STACKADJ(-3);
PyObject_SetItem(v, w, u);
Py_DECREF(u);
Py_DECREF(v);
Py_DECREF(w);

l = [1, 2]
36 LOAD_CONST   0   (1)
39 LOAD_CONST   3   (2)
42 BUILD_LIST   2           // 创建list,并将前两个元素填入list。
45 STORE_NAME   3   (1)

list的创建比较有意思,他会先把list中的元素压入运行时栈,然后通过BUILD_LIST传贱list对象,并把前面的对象(参数为元素的数量)填入list中。
[normal.py]
a = 5
b = a
c = a + b
print c

<consts>
    <int value="5" />
    <NoneObject />
</consts>
<names>
    <internStr index="0" length="1" value="a" />
    <internStr index="1" length="1" value="b" />
    <internStr index="2" length="1" value="c" />
</names>

b = a
0 LOAD_NAME     0   (a)     // 会按顺序从local, globals, builtin三个命名空间中去寻找a
3 STORE_NAME    1   (b)

c = a + b
12 LOAD_NAME    0   (a)
15 LOAD_NAME    1   (b)
18 BINARY_ADD               // 这条指令,表示将上面两个数相加,这里又涉及到加法的快速通道(int和str专用)和慢速通道
19 STORE_NAME   2   (c)

print c
22 LOAD_NAME    2   (c)
25 PRINT_ITEM  
26 PRINT_NEWLINE

[PRINT_ITEM_TO]     // 这条语句主要是用于输出重定向
w = stream = POP();
[if_control.py]
a = 1
if a > 10:
    print "a > 10"
elif a <= -2:="" print="" "a="" <="-2"" elif="" a="" !="1:" 1:="" 1"="" else:="" "unknown="" a"="" if=""> 10:
6 LOAD_NAME     0   (a)
9 LOAD_CONST    1   (10)
12  COMPARE_OP  4   (>)         // 这个字节码为int对象实现了快速通道
15  JUMP_IF_FALSE   9   (to 27)
18  POP_TOP
    print "a > -1"
19  LOAD_CONST  2   ("a>-1")
22  PRINT_ITEM
23  PRINT_NEWLINE
24  JUMP_FORWARD    72  (to 99)
27  POP_TOP
elif a <= 0="" 1="" 3="" 9="" 28="" 31="" 34="" 37="" 40="" -2:="" load_name="" (a)="" load_const="" (-1)="" compare_op="" (<=")" jump_if_false="" (to="" 49)="" pop_top="" 其实大部分判断语句的结构都是相同的="" -="">  LOAD_CONST  ->  COMPARE_OP  ->  JUMP_*
从local命名空间中获得变量名a所对应的值,从常量表consts中读取参与该分支判断操作的常量对象,进行比较获取得结果true/false,根据结果进行指令跳转。
python中的判断语句除了比较语句,还包括了is, in, not in, is not,(还有一个叫PyCmp_EXC_MATCH的东西,好像是和异常处理有关。。。)

list = [1, 2, 3, 4]
if l in list:
LOAD_CONST      0   (1)
LOAD_NAME       0   (list)
COMPARE_OP      6   (in)
JUMP_IF_FALSE
POP_TOP         // 将比较结果移出栈顶

python的比较结果只有两个Py_False和Py_True,底层的实现其实就是0和1。
在Python中,有一些字节码通常都是成对顺序出现的,所以python中还有一个指令预测功能,预测功能通常会用到下面几个宏。
PREDICT(op)
PREDICTED(op)
PREDICTED_WITH_ARG(op)
PEEKARG()

比如PREDICT(JUMP_IF_FALSE)的意思就是检查下一条待处理的字节码是否是JUMP_IF_FALSE。如果是,则程序的流程会跳转到PRED_JUMP_IF_FALSE标志符对应的代码处。
这主要是通过goto语句实现的,也就是说,如果下一句语句是JUMP_IF_FALSE的话,JUMP_IF_FALSE这句字节码就不用再次顺序地进入Python虚拟机的消息循环了,直接通过goto语句跳到case JUMP_IF_FALSE:这个地方,并将指向下一条语句的指针向后移一位。相当于以更快速的方式执行了JUMP_IF_FALSE。

将COMPARE_OP中实现的PREDICT宏展开如下:
[COMPARE_OP]
...
if(*next_instr == JUMP_IF_FALSE)
    goto PRED_JUMP_IF_FALSE;
if(*next_instr == JUMP_IF_TRUE)
    goto PRED_JUMP_IF_TRUE;

JUMPBY(x)   表示下一条字节码的指针移动x个字节。

[for_control.py]
lst = [1, 2]
0   LOAD_CONST  0   (1)
3   LOAD_CONST  1   (2)
6   BUILD_LIST  2
9   STORE_NAME  0   (lst)
for i in lst:
12  SETUP_LOOP  19  (to 34)
15  LOAD_NAME   0   (lst)
18  GET_ITER                    // 获取迭代器
19  FOR_ITER    11  (to 33)
22  STORE_NAME  1   (i)
    print i
25  LOAD_NAME   1   (i)
28  PRINT_ITEM
29  PRINT_NEWLINE   
30  JUMP_ABSOLUTE   19          // 跳转到绝对地址19
33  POP_BLOCK                   // 跳出PyTryBlock,相当于是将PyFrameObject中的f_blockstack数组的当前index向前移动一个,这样简单的方式不对称但是很简单。
34  LOAD_CONST  2   (None)
37  RETURN_VALUE

之前我们谈到过PyFrameObject对象中有一个成员变量叫f_blockstack。
typedef struct _frame   {
    ...
    int f_iblock;
    PyTryBlock  f_blockstack[CO_MAXBLOCKS];
    ...
} PyFrameObject;

typedef struct {
    int b_type;     // block类型
    int b_handler;  // where to jump to find handler
    int b_level;    // value stack level to pop to
} PyTryBlock;
剧透一下PyTryBlock一般用于异常处理和循环O(^_^)O

1/0
#   LOAD_CONST  0
#   LOAD_CONST  1
#   BINARY_DIVIDE

BINARY_DIVIDE其实是调用了一个叫PyNumber_Divide(v, w);的一个函数,如果这个函数返回NULL,表示有异常产生,异常是通过PyErr_SetString(PyExc_ZeroDivisionError, "integer division or modulo by zero")产生。PyExc_ZeroDivisionError这其实是一个PyObject(好吧,这是废话),在Python环境初始化的时候就会被创建。
异常信息会被记录到线程状态对象PyThreadState中。当有异常发生的时候把Python消息循环的标志why设置为有异常发生。并且会沿着PyFrameObject传递,并且会沿反方向记录traceback。而try跟finally会创建两种PyTryBlock,在跳出blockstack后会进入except和finally的PyTryBlock。也就是说,try中的语句会在finnaly和exception的上层执行,退出后肯定会进入下面的PyTryBlock。
另外还有一个有意思的地方就是Python消息循环的函数是递归调用的,一般一个PyCodeObject回对应一个消息循环的入口函数。

本杰明巴顿带给我的启示

发表于 2014-10-06 | 分类于 思考
一件事无论太晚,
或者对于我来说太早,
都不会阻拦你成为你想成为的那个人,
这个过程没有时间的期限,只要你想,随时都可以开始,
要改变或者保留原状都无所谓,做事本不应有所束缚,
我们可以办好这件事,却也可以把它搞砸,
但我希望,最终你能成为你想成为的那个人,
我希望你能驻足于这个令你感到惊叹的世界,体会你从未有过的感觉,
我希望你能见到其他与你观点不同的人们,
我希望你能有一个值得自豪的一生,
如果和你想象的生活不一样,
我希望你能有勇气,
重新启程。

这是我大学里最喜欢的一段话,之前我也跟我朋友说过,我不想过循规蹈矩的生活,可是现实总是残酷的,身边的各种事情逼着你过循规蹈矩的生活,不在意别人的看法是一件很困难的事情。

我想人这一生总是有一段时间是属于自己的,莫名其妙花了这么多年在学校,现在出来了,却要被生活逼着好好工作。

当我自私也好,当我不懂事也好,现在我要过我自己想过的生活。

绝对清晰,是风格上唯一的美

发表于 2014-10-05 | 分类于 反思

今年三月份的时候曾经看过半本《Python源码剖析》,当时就觉得写得很不错。但是由于后来自己太懒惰了,参加实习之后就没有再看了。无论是学一门语言,一个框架,一个操作系统,若你没有看过它的源码,那你永远只能浮于表面,不可能精通它。发现自己做事情总是半途而废

  • 接触Linux算下来也有整整半年时间了,但是也只是会用一小部分最基本的命令。
  • 鸟哥的书(基础篇)看完了,但是大部分因为没有使用都忘记了。
  • tornado源码看了最基础的一部分也就没有看了。
  • 《HTTP权威指南》看到第三部分也就没有再看了。
  • 看了《网络编程》前六章,自己写了一个socket的聊天工具之后也就没有再看了。
  • 看了《高性能Mysql》前六章,后面的因为看不懂也就没看了。(其实看不懂才是成长点的所在。)
  • 标准指法到现在还是没有改过来。(io这两个键,还是经常按错。)

你害怕,是因为你不清晰,如果你清晰了,那么你将无所畏惧。接下来要做的就是把这些坑填完。

在大学的时候总是以为自己学的东西已经饱和了。能够独立完成一整个项目,出来之后才发现,那些都是井底之蛙。一个程序员眼界很重要,这往往决定了一个程序员的命运。

Wake Me Up When Spetember Ends:

  • 送给Python一些小技巧。

十月围城:
送给Python虚拟机了。每次有人问我你最厉害的技能是什么,想了想感觉好像没啥厉害的,等看了Python源码,以后我就能回答这个问题了。

Hello Eleven:

Python的编译结果:pyc文件

发表于 2014-10-05 | 分类于 Python源码剖析

从学编程开始老师就教我们Java是一门编译型语言。而《C#高级编程》中却没有说C#是一门编译型语言。我不知道编译型和解释型语言的明确区别,但是不论是Python还是Java、C#,他们的工作流程都一样,先编译撑一个中间语言,然后交给虚拟机/运行时/解释器来执行命令。Python的编译结果就是今天要讲的pyc文件。pyc文件经常让开发者忽略,而且十分讨厌。

Python的项目中总是带有一大堆的pyc文件,即使你删除了,你重新运行一下项目,他们还是会生成。但是很少有人知道什么时候会生成pyc。python a.py这样是不会生成a.pyc的,pyc文件的生成是通过import来触发的,也就是说,import a这样才会生成pyc文件。如果想知道pyc文件中存了什么,首先必须知道python的工作机制。
普通的python代码是不会运行的,必须编译成字节码,然后把字节码交给python解释器(python虚拟机),python虚拟机把这些字节码翻译成CPU指令。而pyc文件中的就是字节码和一些静态数据。

PyCodeObject对象就是Python编译器编译完py文件后在内存中的对象。PyCodeObject写入文件后就形成了pyc文件。所以PyCodeObject才是真正的编译结果。

typedef struct {
    PyObject_HEAD
    int co_argcount;        // arg的数量
    int co_nlocals;         // 局部变量的数量
    int co_stacksize;       // 需要用到的栈的空间
    int co_flags;           // 不知道干嘛用的
    PyObject *co_code;      // 字节码指令序列,以PyStringObject的形式存在
    PyObject *co_consts;    // PyTupleObject对象,保存这个PyCodeObject中的常量
    PyObject *co_names;     // PyTupleObject 变量名
    PyObject *co_varnames;  // 局部变量的变量名
    PyObject *co_freevars;  // 闭包
    PyObject *co_cellvars;  // 嵌套函数所引用的局部变量名集合
    PyObject *co_filename;  // 对应的文件的完整路径
    PyObject *co_name       // 命名空间的名字
    int co_firstlineno;     // 对应的源文件的这个PyCodeObject的第一行
    PyObject *co_lnotab;    // 字节码与行号的对应关系
    void *co_zombieframe;   //
} PyCodeObject;

PyCodeObject对应的是一个命名空间的代码块,所以一个py文件可以生成很多个PyCodeObject对象,这些PyCodeObject对象是以与代码相应的嵌套结构存放在一个pyc文件中。而pyc中存放的不仅仅是PyCodeObject对象,还存放了magic number和pyc文件的创建时间。magic number是用来确定python的版本的一个特殊数字,pyc创建时间是用来确定是否需要重新生成pyc文件。

PyCodeObject写入文件主要通过了两个基本的方法,w_long(longx, WFILE p)和w_string(char s, int n, WFILE p)方法,而python源码又将这些两个方法组合撑一个对外的方法w_object(PyObject v, WFILE *p)方法。在写入一个变量之前,总是会在前面先写入这个变量的类型,例如TYPE_INT,TYPE_LIST, TYPE_CODE等,这些类型标识以及标识对应的简写符号全部放在import.c文件中。Python在向pyc文件写入字符串的时候比较特殊,要注意interned机制(在pyc中也存在interned机制)。

查看PyCodeObject对象的方法。

>>> f = open("tt.py").read()
>>> co = compile(f, "tt.py", "exec")
>>> dir(co)
>>> co.co_code

另外也能查看一个CodeObjectBlock对应的字节码。


In [8]: import a
"hi"

In [9]: import dis     

In [10]: dis.dis(a)
Disassembly of say:
  5           0 LOAD_CONST               1 ('hello world')
              3 PRINT_ITEM          
              4 PRINT_NEWLINE       
              5 LOAD_CONST               0 (None)
              8 RETURN_VALUE 
1…45
© 2019 Mr.Eleven
由 Hexo 强力驱动
主题 - 由 Mr.Eleven 修改自 NexT.Muse