Python中的Integercaching是什么?

深入Python的源代码之后,我发现它维护了一个从int(-5)到int(256)(@ src / Objects / intobject.c)的PyInt_Object数组,

一个小实validation明了这一点:

 >>> a = 1 >>> b = 1 >>> a is b True >>> a = 257 >>> b = 257 >>> a is b False 

但是如果我把这些代码一起运行在一个py文件中(或者用分号join),结果是不一样的:

 >>> a = 257; b = 257; a is b True 

我很好奇为什么他们仍然是同一个对象,所以我深入挖掘语法树和编译器,我想出了一个调用层次结构如下:

 PyRun_FileExFlags() mod = PyParser_ASTFromFile() node *n = PyParser_ParseFileFlagsEx() //source to cst parsetoke() ps = PyParser_New() for (;;) PyTokenizer_Get() PyParser_AddToken(ps, ...) mod = PyAST_FromNode(n, ...) //cst to ast run_mod(mod, ...) co = PyAST_Compile(mod, ...) //ast to CFG PyFuture_FromAST() PySymtable_Build() co = compiler_mod() PyEval_EvalCode(co, ...) PyEval_EvalCodeEx() 

然后我在PyInt_FromLongPyInt_FromLong之前/之后添加了一些debugging代码,并执行了一个test.py:

 a = 257 b = 257 print "id(a) = %d, id(b) = %d" % (id(a), id(b)) 

输出如下所示:

 DEBUG: before PyAST_FromNode name = a ival = 257, id = 176046536 name = b ival = 257, id = 176046752 name = a name = b DEBUG: after PyAST_FromNode run_mod PyAST_Compile ok id(a) = 176046536, id(b) = 176046536 Eval ok 

这意味着在cstast转换过程中,会创build两个不同的PyInt_Object (实际上它是在ast_for_atom()函数中执行的),但是它们稍后会被合并。

我发现很难理解PyAST_CompilePyEval_EvalCode的源代码,所以我在这里寻求帮助,如果有人提示,我会很感激。

Pythoncaching范围[-5, 256]中的整数,所以期望该范围内的整数也是相同的。

你看到的是Python编译器,当相同的文本的一部分时,优化相同的文字。

在Python shell中input时,每一行都是一个完全不同的语句,在不同的时刻进行分析,因此:

 >>> a = 257 >>> b = 257 >>> a is b False 

但是,如果你把相同的代码放到一个文件中:

 $ echo 'a = 257 > b = 257 > print a is b' > testing.py $ python testing.py True 

只要parsing器有机会分析文字的使用位置,例如在交互式解释器中定义函数时,就会发生这种情况:

 >>> def test(): ... a = 257 ... b = 257 ... print a is b ... >>> dis.dis(test) 2 0 LOAD_CONST 1 (257) 3 STORE_FAST 0 (a) 3 6 LOAD_CONST 1 (257) 9 STORE_FAST 1 (b) 4 12 LOAD_FAST 0 (a) 15 LOAD_FAST 1 (b) 18 COMPARE_OP 8 (is) 21 PRINT_ITEM 22 PRINT_NEWLINE 23 LOAD_CONST 0 (None) 26 RETURN_VALUE >>> test() True >>> test.func_code.co_consts (None, 257) 

注意编译后的代码如何为257包含单个常量。

总而言之,Python字节码编译器不能执行大规模的优化(比如静态types语言),但是它比你想象的要多。 其中一个就是分析文字的使用,避免重复使用。

请注意,这与caching无关,因为它也适用于没有caching的浮点数:

 >>> a = 5.0 >>> b = 5.0 >>> a is b False >>> a = 5.0; b = 5.0 >>> a is b True 

对于更复杂的文字,如元组,它“不起作用”:

 >>> a = (1,2) >>> b = (1,2) >>> a is b False >>> a = (1,2); b = (1,2) >>> a is b False 

但是元组内的文字是共享的:

 >>> a = (257, 258) >>> b = (257, 258) >>> a[0] is b[0] False >>> a[1] is b[1] False >>> a = (257, 258); b = (257, 258) >>> a[0] is b[0] True >>> a[1] is b[1] True 

关于为什么你看到两个PyInt_Object被创build,我这是为了避免字面比较。 例如,数字257可以用多个文字表示:

 >>> 257 257 >>> 0x101 257 >>> 0b100000001 257 >>> 0o401 257 

parsing器有两个select:

  • 在创build整数之前,在一些公共基础上转换文字,并查看文字是否相等。 然后创build一个整数对象。
  • 创build整数对象,看它们是否相等。 如果是,只保留一个值,并为所有的文字分配,否则你已经有整数分配。

可能是Pythonparsing器使用第二种方法,它可以避免重写转换代码,也更容易扩展(例如,它也适用于浮动)。


读取Python/ast.c文件,parsing所有数字的函数是parsenumber ,它调用PyOS_strtoul获取整数值(用于intgers),并最终调用PyLong_FromString

  x = (long) PyOS_strtoul((char *)s, (char **)&end, 0); if (x < 0 && errno == 0) { return PyLong_FromString((char *)s, (char **)0, 0); } 

正如你在这里看到的那样,parsing器不检查它是否已经find了一个具有给定值的整数,所以这就解释了为什么你看到两个int对象被创build,这也意味着我的猜测是正确的:parsing器首先创build常量只有事后优化字节码才能将相同的对象用于相同的常量。

执行此检查的代码必须位于Python/compile.cPython/peephole.c某处,因为这些是将AST转换为字节码的文件。

特别是compiler_add_o函数似乎是这样做的。 在compiler_lambda有这样的注释:

 /* 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; 

所以看起来像compiler_add_o被用来为函数/ ​​lambdas等插入常量。compiler_add_o函数将常量存储到一个dict对象中,并且由此紧接着相同的常量将落入相同的槽中,导致最终的单个常量字节码。