Python 小整数与大整数的处理机制以及整体解释与逐行解释的区别

分析以下代码的执行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env python3
# coding: utf-8

a = 256
b = 256

c = 257
d = 257

def foo():
e = 256
f = 257
g = 257

print('a is b', a is b)
print('c is d', c is d)
print('a is e', a is e)
print('c is f', c is f)
print('f is g', f is g)

foo()

保存为test.py并执行python3 test.py,运行结果如下 :

1
2
3
4
5
a is b True
c is d True
a is e True
c is f False
f is g True

这里先说明下is==的区别:

  • is操作符是比较两个实例对象是不是同一个对象,即内存地址是否相同
  • ==操作符是比较两个实例对象内容是不是一样,即内存地址可能不一样

分析以上结果可知,变量a/bc/da/ef/g是同一个对象。但为什么ae是同一个对象,而cf不是同一个对象?

对以上代码,在终端逐行执行,看看结果会是怎么样的。执行过程及结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> a = 256
>>> b = 256
>>> c = 257
>>> d = 257
>>> def foo():
... e = 256
... f = 257
... g = 257
... print('a is b', a is b)
... print('c is d', c is d)
... print('a is e', a is e)
... print('c is f', c is f)
... print('f is g', f is g)
>>> foo()
a is b True
c is d False
a is e True
c is f False
f is g True

为啥cd不是同一个对象了?

解释以上结果,需要了解Python语言的小整数与大整数的处理机制以及整体解释与逐行解释的区别。

小整数与大整数

学过java的都知道,在JDK5中,为Integer的操作引入了一个新的特性,用来节省内存和提高性能。[-128, 127]之间的整形对象在内部实现中通过使用相同的对象引用实现了缓存和重用。为什么对这个区间范围的整数进行缓存?主要是因为这个范围的整数值使用最广泛(JDK6之后可以通过jvm启动参数设置最大值)。

Python中,同样采用了类似的整形缓存机制。Python将整数的定义细分为小整数和大整数,前者整数值范围为[-5, 257),其余整数为后者。

小整数对象缓存池

Python将小整数缓存到一个特定的small_ints链表中,该链表会存在于Python解释器的整个生命周期,当需要使用小整数时,就去该链表中获取。简单来说,小整数对象会在Python全局解析器范围内被重复引用,且永远不会被GC回收

通用整数对象缓存池

Python运行环境会为大整数对象分配一定的缓存内存空间,该内存空间会被大整数对象轮流使用,直到占满为止,再继续则开辟一块新的内存空间。通过分析Python源码可知,通用整数对象缓存池的结构体定义如下:

1
2
3
4
5
6
7
8
struct _intblock {  
struct _intblock *next;
PyIntObject objects[N_INTOBJECTS];
};
typedef struct _intblock PyIntBlock;

static PyIntBlock *block_list = NULL;
static PyIntObject *free_list = NULL;

PyIntBlock结构体定义了一个PyIntObject数组和指向下一个PyIntBlock结构的指针*next。每个block大约可以存放82个PyIntObjectblock_list用于维护分配给PyIntObject所有的内存空间,free_list则用于维护PyIntObject可用的剩余内存空间。

  • free_listNULL时,Python会重新malloc一个新的block出来。

  • 当一个大整数对象的引用计数为0需要被回收时,其占用的内存空间并不会被回收,而是放到free_list数组中,供新创建的整数对象使用,从而减少内存创建、释放带来的开销。

综上所述,可以解释变量a/bc/da/ef/g为什么是同一个对象。但是还不足以解释c/f以及在终端执行时c/d的结果。这就需要了解Python的解析模式——整体解释和逐行解释。

整体解释与逐行解释

整体解释指的通过应用程序的方式来运行Python代码,而逐行解释是在交互式解释器中执行Python代码。在Python中,解析器的编译单元是一个函数(Python顶层代码也被当作一个函数来进行编译)。每个函数单独编译后会得到一个PyFunctionObject对象,该对象包含了字节码、常量池等信息。

每个PyFunctionObject都拥有一个独立的常量池,如果在同一个PyFunctionObject里创建了值相同的常量,那么这些常量只会在常量池里出现一份。

由此可知,问题中总共包含两个PyFunctionObject对象——由顶层函数和foo函数编译。顶层变量c/d和位于foo函数中的f/g分别位于两个不同的PyFunctionObject对象中,虽然值相同,但是内存地址不同。这就解释了c/f的运行结果。那为什么a/e两个变量是相同的对象了,那是因为小整数是全局解释器缓存的!

那么就剩一个问题了,为什么在交互式命令行终端c/d不是同一个对象了?

那是因为在交互式中,每输入一行语句就会立即执行,即编译单元是一行语句,每执行一行语句就会得到一个PyFunctionObject对象。注意,这里所说的“一行”是指的一次完整性输入。

综上所述,可以得到以下结论:

  • 整体解释是以函数为编译单元的,而交互式终端是以一行语句输入为编译单元,每个编译单元具有独立的常量池
  • 小整数范围为[-5, 257),缓存在全局解释器中,而大整数缓存在编译单元中

问题扩展

修改foo函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/env python3
# coding: utf-8

a = 256
b = 256

c = 257
d = 257

def foo(h=256, i=257):
e = 256
f = 257
g = 257

print('a is h', a is h) # ?
print('c is i', c is i) # ?
print('f is i', f is i) # ?

foo()

运行结果:

1
2
3
a is h True
c is i True
f is i False
感谢你对我的支持,让我继续努力分享有用的技术和知识点