Python中的整数缓存有什么用?


Python中的整数缓存有什么用?

Python在范围内缓存整数[-5, 256],因此预期该范围内的整数也是相同的。

你看到的是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 = (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可以用多个文字表示:

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

解析器有两个选择:

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

读取Python/ast.c文件时,解析所有数字的函数是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);
    }

正如你可以在这里看到解析器将不会检查是否已经找到与给定值的整数,所以这就是为什么你看到两个int对象被创建的,而这也意味着,我的猜测是正确的:解析器首先创建常数并且之后才优化字节码以将相同的对象用于相等的常量。

执行此检查的代码必须位于Python/compile.c或中Python/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对象中,从此紧接着,相等的常量将落在同一个槽中,从而在最终的字节码中产生一个常量。