python天坑分析-对象池+is

前言

这只是一篇技术研究性质的文章,想知道为什么 python 解释器输出结果和我的预期不一样。先贴一段简单代码,引出问题

1
2
3
4
5
6
7
>>> a = 100
>>> b = 100
>>> a is b (True)

>>> a = 257
>>> b = 257
>>> a is b (False)

为什么会不一样?我尝试过做过一些反编译,从字节码层面去找到原因,无奈看不出任何区别,又参考过一些文章,写了一些测试样例,才找到原因,是 python 的对象池机制导致的

懒人版

1、写代码的时候尽量不要用 is 关键词去做比较,除了 is None 场景和 bool 值判断场景

2、在字节码层面查不出原因

3、引起区别的原因在于解释器层面,需要去看 python 解释器源码,他里面引入了对象池机制来做部分数据的缓存,对象池触发机制有一定限制,导致结果和我们预期的有冲突

4、python 交互模式,不同行处在不同的代码块,而在 ide 中,如 sublime 或者 pycharm 这样的两行代码并不会定义在不同代码块中,因此在 python 交互模式中更容易重现该问题

勤劳版

要想解决这个问题,最好提前了解一下 python 中 is 关键字,字节码,代码块,相关的概念

关于 is 关键字

python 中的 is 关键词实际上比较的是他的内存地址是否一样,而 == 比较的是他两个值是否一样,内存地址我们可以用 python 内建函数 id() 获取

代码块

代码块官方解释

A block is a piece of Python program text that is executed as a unit. The following are blocks: a module, a function body, and a class definition. Each command typed interactively is a block. A script file (a file given as standard input to the interpreter or specified on the interpreter command line the first argument) is a code block. A script command (a command specified on the interpreter command line with the ‘-c’ option) is a code block. The file read by the built-in function execfile() is a code block. The string argument passed to the built-in function eval() and to the exec statement is a code block. The expression read and evaluated by the built-in function input() is a code block.

A code block is executed in an execution frame. A frame contains some administrative information (used for debugging) and determines where and how execution continues after the code block’s execution has completed.

python 在执行前会把 .py 文件编译为字节码 .pyc 文件,然后再由 python 解释器去执行,实际上 .pyc 中存储的就是代码块的内容,也就是多个 PyObject 对象,对象池的触发机制我喜欢按照代码块去划分,同一个代码块中什么情况下触发,不同代码块中什么情况下触发

python 交互模式中的代码块

上面的代码当我们用 pycharm 或者 sumblime 执行是完全不同的结果,这里涉及到 python 交互模式和其他 ide 对代码块定义不一样,官方文档在代码块解释中有说明,这段代码解释了交互模式下,同一个代码块才可以触发大整数池机制

1
2
3
4
5
6
>>> a = 257
>>> b = 257
>>> a is b
False
>>> a = 257;b = 257;print(a is b)
True

在 python 交互模式中按照上面方式分别定义,a,b,被定义到不同代码块,第二种定义方式 a,b 被定义到了同一个代码块,结果为啥不一样会在后面说明

ide 中的代码块

后面为了方便 coding,样例代码多数都是在 ide 中编写,执行,这段代码展示了不同代码块情况下,触发了小整数对象池机制,不能触发大整数对象池机制

1
2
3
4
5
6
7
8
9
10
11
12
13
a = 200
print(a is 200) # True
def myfunc():
return a is 200

print(myfunc()) # True

a = 257
print(a is 257) # True
def myfunc():
return a is 257

print(myfunc()) # False

官方文档中说明了,函数,类,模块,都可以当做一个代码块,因为定义的额变量 a 是在函数外部,因此就涉及到了多个代码块

对代码块中的内容好奇一下

一个代码块对应的其实就是一个 PyCodeObject 对象,这个对象有多个属性,如常量,变量,想看到代码块中的详细内容,有办法

PyCodeObject 对象查看工具

依赖这个工具,我们可以看到 PyCodeObject 对象的一些属性,有几个常量分别是哪些,索引号多少,有哪些变量,被引用的次数,这里展开说就扯远了,专门说字节码和 PyCodeObject 的联系,对解决了解当前问题没啥帮助

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="utf-8"?>
<codeobject>
<co_consts count="6">
<item idx="0">-1</item>
<item idx="1">None</item>
<item idx="2">256</item>
<item idx="3">255</item>
<item idx="4">1</item>
<item idx="5">256</item>
</co_consts>
<co_names count="4">
<name idx="0">sys</name>
<name idx="1">dis</name>
<name idx="2">y</name>
<name idx="3">x</name>
</co_names>
<co_varnames count="0"/>
<co_filename>memory_check.py</co_filename>
<co_ename>&lt;module&gt;</co_ename>
<co_nlocals>0</co_nlocals>
<co_stacksize>2</co_stacksize>
<co_argcount>0</co_argcount>
</codeobject>

误入歧途之字节码层面审查问题

最初我想用 python 自带的 dis 模块在字节码层面去看这个问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
a = 200
def myfunc():
return a is 200

print(myfunc())
dis.dis(myfunc)

a = 257
def myfunc():
return a is 257

print(myfunc())
dis.dis(myfunc)

# 结果

True
19 0 LOAD_GLOBAL 0 (a)
3 LOAD_CONST 1 (200)
6 COMPARE_OP 8 (is)
9 RETURN_VALUE
False
26 0 LOAD_GLOBAL 0 (a)
3 LOAD_CONST 1 (257)
6 COMPARE_OP 8 (is)
9 RETURN_VALUE

通过 dis 模块,同样的字节码,只有 LOAD_CONST 值不一样,但是他们的执行结果缺不一样,知道问题没那么简单了,需要从解释器层面去找问题了

对象池相关数据类型对象池触发机制

翻了一下书,查了一些资料,为了节约系统资源,python 里面有一个对象池机制,方便一些变量值的重复使用,因为我本地 python 版本为 2.7.10,专门去下载了一个对应的源码包,方便分析问题,不同版本代码有些许区别

整数类型

小整数类型

小整数池在 python 语言运行初始化的时候会初始化,然后常驻内存 [-5, 257)

小整数类型官方说明文档

官方文档描述如下:

PyObject* PyInt_FromLong(long ival) Return value: New reference. Create a new integer object with a value of ival.

The current implementation keeps an array of integer objects for all integers between -5 and 256, when you create an int in that range you actually just get back a reference to the existing object.

So it should be possible to change the value of 1.

I suspect the behaviour of Python in this case is undefined. :-)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//定义了两个宏,实际上也就是两个临界值了
#ifndef NSMALLPOSINTS
#define NSMALLPOSINTS 257
#endif
#ifndef NSMALLNEGINTS
#define NSMALLNEGINTS 5
#endif

//定义了一个长度为 NSMALLNEGINTS + NSMALLPOSINTS 的数组 small_ints
static PyLongObject small_ints[NSMALLNEGINTS + NSMALLPOSINTS];

PyObject *
PyInt_FromLong(long ival)
{
register PyIntObject *v;
#if NSMALLNEGINTS + NSMALLPOSINTS > 0
if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS) {
//从数据中获取数据
v = small_ints[ival + NSMALLNEGINTS];
Py_INCREF(v);
#ifdef COUNT_ALLOCS
if (ival >= 0)
quick_int_allocs++;
else
quick_neg_int_allocs++;
#endif
return (PyObject *) v;
}
#endif
if (free_list == NULL) {
if ((free_list = fill_free_list()) == NULL)
return NULL;
}
/* Inline PyObject_New */
v = free_list;
free_list = (PyIntObject *)Py_TYPE(v);
PyObject_INIT(v, &PyInt_Type);
v->ob_ival = ival;
return (PyObject *) v;
}
//如果大小是 [-5, 257) 之间直接去数组中获取已有对象
小整数类型对象池的触发机制

1、同一个代码块,不同代码块,整数大小介于 [-5, 257) 才会触发

大整数类型

1
2
3
4
5
6
7
8
y = 256
x = 255 + 1
print(x is y) # True


y = 257
x = 256 + 1
print(x is y) # False
大整数类型对象池的触发机制

1、同一个代码块中,不涉及到运算,才触发

字符串类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> a = "abc"
>>> b = "abc"
>>> a is b
True

>>> a = "hello world"
>>> b = "hello world"
>>> a is b
False

>>> a = 'test' * 5
>>> b = 'test' * 5
>>> a is b
True

>>> a = 'test' * 6
>>> b = 'test' * 6
>>> a is b
False

乘数 >=2 时:

字符串类型对象池的触发机制

1、不同代码块中要触发字符串类型对象池机制,不能含有空格等特殊字符串(特殊字符串只有下划线)

2、同一个代码块中都会触发

3、字符串 + * 运算符 数字等于 1 时,触发

4、字符串 + * 运算符 数字大于 1 时,包含数字,字幕,下划线,且总长度小于等于 20 才触发

这里实际上总结的并不详细,我只是随便举了几个例子,也没必要去纠结总结所有情况,了解一下就好

其他相关

手工编译 python

甚至可以在 python 源码中加上一些 print 函数,然后编译,再运行,可以更直观的看到效果

手工触发对象池机制

在 python3 中引入了 intern 模块,可以手工触发对象池机制,让某些字符串常驻内存,看起来只对字符串适用

1
2
3
4
5
6
7
8
9
10
>>> from sys import intern
>>> a = "hello world"
>>> b = "hello world"
>>> a is b
False

>>> a = intern("hello world")
>>> b = intern("hello world")
>>> a is b
True

总结

1、对象池触发机制过于复杂,我不想去挑战记住那么多规则,所以舍弃使用 is,只有 bool,None 的情况下试用 is 来做判断

2、针对网上的一些说法进行了汇总,一些不正确、很片面的说法进行了更正,部分核心说明都在官网或者源码处找到了对应出处

3、尝试从字节码层面去查看问题,虽然失败了,但过程还是蛮有趣

坚持原创技术分享,您的支持将鼓励我继续创作!