Python小整数对象池和字符串intern实例解析
作者:luoheng
is用于判断两个对象是否为同一个对象,具体来说是两个对象在内存中的位置是否相同。
python为了提高效率,节省内存,在实现上大量使用了缓冲池技术和字符串intern技术。
整数和字符串是不可变对象,也就意味着可以用来共享,如100个“python”字串变量可以共享一个“python”字符串对象,而不是创建100个“python”字符串。
小整数对象池
为了应对小整数的频繁使用,python使用对小整数进行了缓存,默认范围为[-5,256],在这个范围内的所有整数被python完全地缓存,当有变量使用这些小整数时,增加对应小整数对象的引用即可。
>>> i = -5 >>> j = -5 >>> i is j # i和j是同一个对象 True >>> i = 256 >>> j = 256 >>> i is j # i和j是同一个对象 True >>> i = 257 >>> j = 257 >>> i is j # i和j是不同对象 False
由上面的实例可以看到,当变量在[-5,256]之间时,两个值相同的变量事实上会引用到同一个小整数对象上,也就是小整数对象池中的对象,而不会去创建两个对象。而当变量超出了这个范围,两个值相同的变量也会各自创建整数对象,所以两者对应的对象不同。
字符串intern
如果当前变量引用的字符串对象已经存在的话,直接增加对应字符串对象的引用,而不去创建新的字符串对象,这就是字符串intern机制。
>>> i = "12"
>>> j = "12"
>>> i is j
True
在详细探讨字符串intern机制之前,先看一个奇怪的问题:
>>> i = "1 2"
>>> j = "1 2"
>>> i is j
False
i = "1 2"
j = "1 2"
print(i is j)
输出结果
True
上述代码分开运行,结果为False,但是合在一起结果却为True,也就是说分开运行的时候,i,j指向不同对象,而合在一起的时候i,j却指向了相同对象。为了明白其中的缘由,需要简单理解python的编译机制。
编译机制
在python中,万物皆对象,包括代码本身也是一种对象。python用code对象表示代码,代码编译后产生code对象。通常一个作用域对应一个code对象。
i = "1 2" j = "1 2" print(i is j) def f(): pass
编译结果
2 0 LOAD_CONST 0 ('1 2')
2 STORE_NAME 0 (i)3 4 LOAD_CONST 0 ('1 2')
6 STORE_NAME 1 (j)5 8 LOAD_CONST 1 (<code object f at 0x00000200F257CF60, file "small_int.py", line 5>)
10 LOAD_CONST 2 ('f')
12 MAKE_FUNCTION 0
14 STORE_NAME 2 (f)
16 LOAD_CONST 3 (None)
18 RETURN_VALUEDisassembly of <code object f at 0x00000200F257CF60, file "small_int.py", line 5>:
6 0 LOAD_CONST 0 (None)
2 RETURN_VALUE
上述代码中编译生成了两个code对象,一个代表全局作用域,另一个代表函数f。
code对象保存了变量,常量(常量字面量)以及编译结果。code对象用常量表来保存常量,考虑到一个常量可能出现多次,在一张表上保存一个常量多次太过于奢侈。所以code对象对每个常量只保存一次,在需要引用它的地方使用它在常量表的位置作为常量的表示。在上述编译结果中可以看到,"1 2"这个字符串常量使用了两次,编译的代码为"LOAD_CONST 0",这里的0就是"1 2"在常量表当中的位置。
由于编译的这个特性,在同一个code对象中的变量,如果它们引用了同一个常量,那么无论这个常量有没有缓冲机制,它们引用的都是同一个对象。
a = "12" b = "12" c = "1 2" d = "1 2" e = 257 f = 257 g = 2424234234234234 h = 2424234234234234 print(a is b, c is d, e is f, g is h)
输出结果
True True True True
这个例子说明,在同一个code对象当中,常量(字面量)仅一份,这与缓冲机制无关,是编译特性。所以对于上述那个奇怪的问题就可以解释了,当i,j在同一个code对象(同一个作用域)中引用常量"1 2",它们引用的都是同一个对象。而当在python命令行中分开执行时,对于每一条语句,都是一个单独的code对象,这时起作用的是字符串intern机制,上述运行结果说明,字符串intern机制对"12"进行了intern,而对"1 2"没有进行intern。
编译机制与小整数对象池对比
i = 257 j = 257 a = i - 1 b = i - 1 c = i + 1 d = i + 1 print(i is j, a is b, c is d)
输出结果
True True False
i和j引用同一个常量,这是编译机制,所以i与j指向同一个整数对象,后面a和b虽然相等,但不引用常量,此时启用小整数对象池,a,b都等于256,在对象池中,所以a,b引用同一个对象,后面c,d不在对象池中,所以两者对象不同。
这里有一点需要注意,没有变量参与的运算会被编译器直接优化成对应的常量,进而保存进常量表中。
字符串intern机制与字符缓冲池
在编译过程中,字符串intern机制将所有的变量名进行intern,但对常量进行的intern有一点特殊的限制。能够intern的常量必须只包含[a-zA-Z0-9_],即字母数字加下划线,如果含有其他字符,就不会intern。在运行过程中,通过计算得到的字符串不会intern。
字符串有一个和小整数对象池相似的字符缓冲池,用于在运行过程中缓存单个字符,所以计算得到的字符串虽然不会intern,但如果是单个字符,就会使用到字符缓冲池。
k = "bbb" a = k[0] b = k[0] c = k[1:] d = k[1:] print(a, d) print(a is b, c is d)
输出结果
b bb
True False
可以看到,a和b确实指向同一个对象,而c和d指向不同对象,这就是字符缓冲池。
编译机制与字符串intern对比
i = "1 2" j = "12" k = "__fjdslfjaskfas" ii = "1 2" jj = "12" kk = "__fjdslfjaskfas" def f(): a = "1 2" b = "12" c = "__fjdslfjaskfas" return a is i, b is j, c is k print("Code:", i is ii, j is jj, k is kk) print(f"intern: {f()}")
输出结果
Code: True True True
intern: (False, True, True)
i包含空格,包含空格的常量不会被intern,而其他两个常量不包含其他字符,所以会被intern。
总结
1. python代码被编译成code对象,通常一个code对象对应于一个作用域,作用域中重复出现的变量名以及常量在code中只保存一次。
2. 字符串intern机制主要作用于编译过程,在编译收集完变量和常量时,对变量和常量进行intern,而后构建一个code对象。
3. 字符串intern对常量的intern有限制,能够intern的常量必须只包含[a-zA-Z0-9_],即字母数字加下划线,如果含有其他字符,就不会intern。
4. 小整数对象池和字符缓冲池都是作用于运行过程中,python缓存小的整数和字符,当有变量使用这些对象时,不用额外创建对象。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。