标题:Python int缓存的那点事[续] 出处:Felix021 时间:Thu, 07 Mar 2013 02:57:51 +0000 作者:felix021 地址:https://www.felix021.com/blog/read.php?2109 内容: 上一篇 说到,对于这样的一段代码:a = 257 b = 0x101 print a is b Python解释器会为 a 和 b 各 创建一个 PyIntObject (通过修改PyInt_FromLong打印int的id可以看出来),但是在实际的执行中,a和b却指向了同一个PyIntObject。也就是说,在执行之前,a和b已经被映射到了同一个PyIntObject。 前面说了,Python的解释执行是由以下调用链组成的: 引用 PyRun_FileExFlags() mod_ty *mod = PyParser_ASTFromFile() //把py源码转换成AST(Abstract Syntax Tree) run_mod(mod, ...) //执行AST co = PyAST_Compile(mod, ...) //将AST转换成CFG(Control Flow Graph) bytecode PySymtable_Build() //创建符号表 co = compiler_mod() //编译ast为bytecode PyEval_EvalCode(co, ...) //执行bytecode PyEval_EvalCodeEx() 由于没有编译原理的基础,只能从全局上看出这些代码都做了什么,但是却很难从细节上去追查。通过修改源码我尽可能了解了 PyParser 将Python源码转换成AST的运行机制(虽然还是没有看懂tokens->cst的转换),但是run_mod的细节实在是看不懂了。于是我在StackOverflow上面提了一个问题,@Bakuriu大牛给了个hint,说是PyCompiler在处理lambda的时候,使用 compiler_add_o() 来将lambda对应的函数的__doc__设置为 PyNone:/* Make None the first constant, so the lambda can't have a docstring. */ if (compiler_add_o(c, c->u->u_consts, Py_None) < 0) return 0; 这里头PyNone是常量,而且又出现了 c->u->u_consts ,大有看头。 有了线索以后,突然一切都变得清晰了,简单加了些代码追,可以发现对于上面给出的代码, compiler_mod 是这样处理mod_ty *mod(也就是那棵AST)的: 引用 compiler_mod(compiler *c, mod_ty *mod) case Module_kind: //mod->kind = 1 compiler_body(c, mod->v.Module.body stmts) for (i = 0; i < asdl_seq_LEN(stmts); i++) //循环3次,因为有3个stmt VISIT(c, stmt, asdl_seq_GET(stmts, i)) //宏展开到compiler_visit_stmt compiler_visit_stmt(c, asdl_seq_GET(stmts,i) s)//访问每个stmt case Assign_kind: //第一个stmt的kind = 5,表示一个赋值操作 //赋值操作允许 a = b = 1 所以看起来有点罗嗦, //它会被解析成如下AST: //Assign([AssName('a', 'OP_ASSIGN'), // AssName('b', 'OP_ASSIGN')], Const(1)) n = asdl_seq_LEN(s->v.Assign.targets); VISIT(c, expr, s->v.Assign.value); for (i = 0; i < n; i++) { if (i < n - 1) ADDOP(c, DUP_TOP); VISIT(c, expr, asdl_seq_GET(s->v.Assign.targets, i)); } 简单解释下Assign操作的代码: 1. 获得赋值目标的数量(比如a=b=1,就是2个) 2. "VISIT"要赋的值 3. 挨个ADDOP(c, DUP_TOP)是告诉编译器,增加一个OPCODE=DUP_TOP DUP_TOP 是 Duplicates the reference on top of the stack 的简写,意思是取得上次计算的值(比如对于b,就是int(1)的reference,而对于a,就是b=1的返回值,也就是b的reference)加入stack_top,这样正好把多个赋值操作串起来。 不过我关注的主要是第二条,对应的代码就是: 引用 VISIT(c, expr, s->v.Assign.value); //宏展开到compiler_visit_expr compiler_visit_expr(c, s->v.Assign.value e) case Num_kind: //e->kind = 16 ADDOP_O(c, LOAD_CONST, e->v.Num.n, consts) //宏展开到compiler_addop_o //这里e->v.Num.n是在CST->AST的过程中生成的PyIntObject //下面的c->u->u_consts是一个PyDictObject,用来保存常量对象 compiler_addop_o(c, LOAD_CONST opcode, c->u->u_consts dict, e->v.Num.n o) arg = compiler_addop_o(c, dict, o)//塞入dict compiler_addop_i(c, opcode, arg) //将插入顺序作为opcode的oparg 这个compiler_add_o(struct compiler *c, PyObject *dict, PyObject *o)的作用是将一个变量o及其type组成的tuple(o, o->ob_type)塞入到dict中。但是并不是简单暴力地直接插入,它的源码大约是这样的: static int compiler_add_o(struct compiler *c, PyObject *dict, PyObject *o) { PyObject *t, *v; Py_ssize_t arg; if (PyFloat_Check(o)) { //省略部分与int无关的代码 } else { t = PyTuple_Pack(2, o, o->ob_type); //t = tuple(o, type(o)) } if (t == NULL) return -1; v = PyDict_GetItem(dict, t); //看看t是否已经在dict中出现过 if (!v) { //如果没有 arg = PyDict_Size(dict); //获取dict的当前大小(PyIntObject) v = PyInt_FromLong(arg); if (!v) { Py_DECREF(t); return -1; } if (PyDict_SetItem(dict, t, v) < 0) { //dict[(o, type(o))] = v Py_DECREF(t); Py_DECREF(v); return -1; } Py_DECREF(v); } else arg = PyInt_AsLong(v); //如果出现过,取得之前设置的v Py_DECREF(t); return arg; } 这个代码乍看挺诡异的,因为还与后续编译成字节码的部分有所耦合,这里大致解释一下: (1) 对于LOAD_CONSTS, 在compiler_visit_expr里面已将dict指定为c->u->u_consts,也就是专门用来存放常量的dict (2) 所有常量都是用 (o, type(o)) 作为 key 存进去的,并返回顺序递增的编号v,表示o是第v个存进去的常量 (3) dict是个hash表,所以往dict里面塞东西时要计算key的hash (4) tuple的hash值是将每个元素的hash值组合起来哈希(详见tuplehash函数),类似于sdbmhash或者jshash (5) int对象的哈希是针对int的值,int对象比较时仅比较它们的值 尽管 a 和 b 对应的key (a, int)和(b, int)虽然不是同一个对象,但是它们的哈希值是一样的!并且!PyDict_GetItem()查找到某个slot去比较key的时候,递归地去比较key的每一个元素,而两个int的比较,“正好”是比较它们的值是否相等! 所以,在遍历AST生成bytecode之前,两个相同的const只会在c->u->u_consts中出现1次。 在compiler_mod函数的末尾有一个assemble(c, addNone),它是将前面生成好的opcode等数据转换成最终的bytecode,其中一些代码逻辑是这样的 assemble(c, addNone) struct assembler a; 完成一些初始化 makecode(c, &a); PyObject *tmp = dict_keys_inorder(c->u->u_consts, 0); //convert to Tuple consts = PySequence_List(tmp); //convert to List ... PyCodeObject *co = PyCode_New(..., consts, ...); co->co_consts = consts 也就是说,这里把保存所有常量的 c->u->u_consts 按照插入元素的顺序将所有塞进来的PyObject逐个插入到 consts 里,并最后赋值给PyCodeObject的PyListObject *co_consts。而在最最后的eval环节,LOAD_CONST这个opcode会将它的oparg(就是前面 compiler_addop_i 塞进去的值,也就是compiler_addop_o返回的值)作为索引,从co_consts里取出来,PUSH到栈顶(参见Python/ceval.c +1123行),供下一个指令读取。 于是int常量整个python的解释执行所经历的步骤都完整地串起来了。 泪流满面,居然看懂了。 Generated by Bo-blog 2.1.0