Python 什么';是否使用解释器维护的整数缓存?

Python 什么';是否使用解释器维护的整数缓存?,python,caching,code-analysis,literals,python-internals,Python,Caching,Code Analysis,Literals,Python Internals,在深入研究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文件中同时运行这些

在深入研究Python的源代码之后,我发现它维护了一个
PyInt_Object
s数组,范围从
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\u ParseFileFlagsEx()//源到cst
parsetoke()
ps=PyParser_New()
对于(;;)
PyTokenizer_Get()
PyParser_AddToken(ps,…)
mod=PyAST_FromNode(n,…)//cst到ast
运行模式(模式,…)
co=PyAST\u Compile(mod,…)//ast to CFG
PyFuture_FromAST()
PySymtable_Build()
co=编译器_mod()
PyEval_EvalCode(co,…)
PyEval_evalcodex()
然后,我在
PyInt\u FromLong
PyAST\u FromNode
之前/之后添加了一些调试代码,并执行了一个test.py:

a = 257
b = 257
print "id(a) = %d, id(b) = %d" % (id(a), id(b))
输出如下所示:

DEBUG:在PyAST\u FromNode之前
名称=a
ival=257,id=176046536
name=b
ival=257,id=176046752
名称=a
name=b
调试:在PyAST_FromNode之后
运行模式
PyAST_编译正常
id(a)=176046536,id(b)=176046536
评估正常
这意味着在
cst
ast
转换过程中,会创建两个不同的
PyInt\u对象
s(实际上是在
ast\u for_atom()
函数中执行),但随后会将它们合并


我发现很难理解
PyAST\u Compile
PyEval\u EvalCode
中的源代码,所以我来这里寻求帮助,如果有人给出提示,我将不胜感激。

Python缓存范围内的整数,因此该范围内的整数通常是相同的

对于257,您看到的是Python编译器在同一代码对象中编译时优化相同的文本

在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 = (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
(注意,常量折叠和窥视孔优化器甚至可以在bugfix版本之间改变行为,因此返回
True
False
的示例基本上是任意的,将来会改变)


关于为什么会创建两个
PyInt_对象
,我猜这样做是为了避免文字比较。例如,数字
257
可以用多个文字表示:

>>> 257
257
>>> 0x101
257
>>> 0b100000001
257
>>> 0o401
257
解析器有两种选择:

  • 在创建整数之前,将文本转换为一些公共基,然后查看文本是否等效。然后创建一个整数对象
  • 创建整数对象并查看它们是否相等。如果是,则只保留一个值并将其分配给所有文本,否则,您已经有了要分配的整数
Python解析器可能使用第二种方法,这避免了重写转换代码,而且更易于扩展(例如,它也可以使用浮点)


读取
Python/ast.c
文件时,解析所有数字的函数是
parsenumber
,它调用
PyOS\u strtoul
以获取整数值(对于整数),并最终从字符串调用
PyLong\u

    x = (long) PyOS_strtoul((char *)s, (char **)&end, 0);
    if (x < 0 && errno == 0) {
        return PyLong_FromString((char *)s,
                                 (char **)0,
                                 0);
    }
因此,
compiler\u add\u o
似乎用于为函数/lambda等插入常量。
compiler\u add\u o
函数将常量存储到
dict
对象中,随后相等的常量将落在同一个插槽中,从而在最终字节码中产生一个常量。

Python缓存该范围内的整数,因此该范围内的整数通常是相同的

对于257,您看到的是Python编译器在同一代码对象中编译时优化相同的文本

在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 = (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
(注意,常数折叠和窥视孔优化器甚至可以在错误修复版本之间改变行为,因此哪些示例返回
True
False