深入研究Python的源代码后,我发现它维护了一个PyInt_Objects 数组,范围从int(-5)到int(256)(@ src / Objects / intobject.c)
Python
PyInt_Objects
int(-5)到int(256)(@ src / Objects / intobject.c)
一个小实验证明了这一点:
>>> a = 1 >>> b = 1 >>> a is b True >>> a = 257 >>> b = 257 >>> a is b False
但是,如果我在py文件中一起运行这些代码(或使用分号将它们结合在一起),结果将有所不同:
>>> 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_FromLong之前/之后添加了一些调试代码PyAST_FromNode,并执行了一个test.py:
PyInt_FromLong
PyAST_FromNode
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
这意味着,在cst以ast变换,两个不同的PyInt_Objects的创建(实际上它的真实执行的ast_for_atom()功能),但他们后来合并。
cst
ast
ast_for_atom()
我觉得很难理解的来源PyAST_Compile和PyEval_EvalCode,所以我在这里寻求帮助,如果有一个人给了一个暗示,我会感激?
PyAST_Compile和PyEval_EvalCode
Python会缓存范围内的整数[-5, 256],因此可以预期该范围内的整数也相同。
[-5, 256]
你看到的是Python编译器在相同文本的一部分时优化了相同文字。
在Python shell中键入时,每行都是完全不同的语句,在不同的时刻进行了解析,因此:
Python shell
>>> a = 257 >>> b = 257 >>> a is b False
但是,如果将相同的代码放入文件中:
$ echo 'a = 257 > b = 257 > print a is b' > testing.py $ python testing.py True
每当解析器有机会分析使用文字的位置时(例如在交互式解释器中定义函数时),都会发生这种情况:
>>> 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字节码编译器无法执行大规模优化(如静态类型语言),但它的功能超出你的想象。这些事情之一是分析文字的用法并避免重复它们。
请注意,这与缓存无关,因为它也适用于没有缓存的浮点数:
>>> 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被创建的原因,我猜想这样做是为了避免字面比较。例如,数字257可以用多个文字表示:
PyInt_Object
>>> 257 257 >>> 0x101 257 >>> 0b100000001 257 >>> 0o401 257
解析器有两种选择:
在创建整数之前,将文字转换为某些通用基数,然后查看文字是否等效。然后创建一个整数对象。 创建整数对象,然后查看它们是否相等。如果是,则仅保留一个值并将其分配给所有文字,否则,你已经具有要分配的整数。 Python解析器可能使用了第二种方法,该方法避免了重写转换代码,并且更易于扩展(例如,它也可以与float一起使用)。
float
读取Python/ast.c文件后,解析所有数字的函数是,该函数parsenumber调用PyOS_strtoul以获得整数值(对于整数),并最终调用PyLong_FromString:
Python/ast.c
parsenumber
PyOS_strtoul
PyLong_FromString
x = (long) PyOS_strtoul((char *)s, (char **)&end, 0); if (x < 0 && errno == 0) { return PyLong_FromString((char *)s, (char **)0, 0); }
正如你可以在这里看到解析器将不会检查是否已经找到与给定值的整数,所以这就是为什么你看到两个int对象被创建的,而这也意味着,我的猜测是正确的:解析器首先创建常数并且只有在此之后,才能优化字节码以将相同的对象用于相同的常量。
进行此检查的代码必须位于Python/compile.c或中Python/peephole.c,因为这些是将AST转换为字节码的文件。
Python/compile.c
Python/peephole.c
特别是,该compiler_add_o功能似乎是执行此功能的人。中有此评论compiler_lambda:
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对象中,然后立即将相等的常量放入同一插槽中,从而在最终字节码中生成单个常量。
/ lambdas
dict