从源码角度理解Python的is和判断的底层区别
目录导读
- 引言:一个困扰初学者的经典问题
- 底层机制解析:与
is的Python源码实现 - 核心差异:值相等 vs 身份相等
- 源码级对比:
PyObject_RichComparevsPyObject_RichCompareBool - 经典陷阱:整数缓存、字符串驻留与可变对象
- 实战问答:20个高频场景测试你的理解
- 性能与安全:何时该用
is,何时该用 - 写出更健壮的Python代码
一个困扰初学者的经典问题
在Python面试和日常开发中,a == b和a is b的区别几乎是必考题,表面上,比较“值”,is比较“身份”,但为什么5 == 5.0返回True,而5 is 5.0返回False?为什么a = 256和b = 256时a is b为True,但a = 257时却为False?要彻底理解,必须深入CPython源码。
底层机制解析:与is的Python源码实现
在CPython(Python的官方解释器)中,is和的实现路径完全不同。
1 is运算符的源码路径
is对应字节码COMPARE_OP中的IS_OP,核心实现在Python/ceval.c的case TARGET(IS_OP)分支中:
case TARGET(IS_OP): {
PyObject *right = POP();
PyObject *left = TOP();
PyObject *b = oparg ? Py_True : Py_False;
PyObject *res = (left == right) ? b : (b == Py_True ? Py_False : Py_True);
...
}
关键点:is直接比较两个对象的内存地址(即left == right是指针相等),它不会调用任何__eq__方法。
2 运算符的源码路径
对应COMPARE_OP中的Py_EQ,核心实现在Objects/object.c的PyObject_RichCompare函数中:
PyObject * PyObject_RichCompare(PyObject *v, PyObject *w, int op) {
...
// 首先尝试调用 PyTypeObject 中的 tp_richcompare 槽位
if (Py_TYPE(v)->tp_richcompare != NULL) {
res = (*Py_TYPE(v)->tp_richcompare)(v, w, op);
...
}
...
}
tp_richcompare通常指向object_richcompare,它会先检查两个对象是否完全一致(指针相等),然后根据类型调用自定义的__eq__方法。
核心差异:值相等 vs 身份相等
| 特性 | (值相等) | is(身份相等) |
|---|---|---|
| 比较依据 | 调用__eq__方法 |
比较内存地址(id) |
| 可覆盖性 | 可被自定义类重写 | 不可被重写 |
| 默认行为 | 如果未定义__eq__,退化为is |
始终比较身份 |
| 典型用途 | 比较数值、字符串内容 | 比较单例(None、True、False) |
底层代码印证:在Objects/typeobject.c的object_richcompare中:
static PyObject * object_richcompare(PyObject *v, PyObject *w, int op) {
...
if (v == w) {
// 如果两个对象是同一个,直接返回True/False
if (op == Py_EQ) Py_RETURN_TRUE;
if (op == Py_NE) Py_RETURN_FALSE;
}
// 否则检查是否实现了 __eq__
...
}
源码级对比:PyObject_RichCompare vs PyObject_RichCompareBool
Python内部存在两个相关函数:
PyObject_RichCompare:返回PyObject*(可能是True/False或NotImplemented)PyObject_RichCompareBool:返回int(1表示真,0表示假,-1表示异常)
运算符最终调用PyObject_RichCompareBool,而is直接走指令表。
性能差异:is比快一个数量级,因为它不需要方法查找和tp_richcompare调用链,在Python层面测试:
import timeit
print(timeit.timeit('a is b', 'a=b=256', number=10_000_000)) # ~0.3s
print(timeit.timeit('a == b', 'a=b=256', number=10_000_000)) # ~0.6s
经典陷阱:整数缓存、字符串驻留与可变对象
1 小整数缓存([-5, 256])
在Objects/longobject.c的_PyLong_Init中,Python预创建了从-5到256的整数对象:
// 源码片段
for (i = -5; i <= 256; ++i) {
v = _PyLong_FromSmallPythonInt(i);
small_ints[i_plus_5] = v;
}
因此a = 256; b = 256时,a is b为True,但a = 257; b = 257时,每次都会新建对象,a is b为False。
2 字符串驻留(Intern)
Objects/unicodeobject.c中的intern_inplace函数会缓存字符串字面量:
// 源码片段
if (PyUnicode_CheckExact(t) && PyUnicode_READY(t) == 0) {
if (interned == NULL) { interned = PyDict_New(); ... }
PyObject *t_interned = PyDict_GetItem(interned, t);
if (t_interned != NULL) { ... }
else { PyDict_SetItem(interned, t, t); ... }
}
所以'hello' is 'hello'通常为True,但'hello ' + 'world' is 'hello world'可能为False,因为拼接结果不会被驻留。
3 可变对象的陷阱
列表、字典等可变对象不会缓存:
a = [1, 2]; b = [1, 2] a == b # True(值相等) a is b # False(不同对象)
实战问答:20个高频场景测试你的理解
Q1:0 is False返回什么?
A:False,因为False是bool类型对象(值为0),0是int类型对象,虽然0 == False为True,但它们是不同对象。
Q2:0 is 1返回什么?
A:False,浮点数和整数永远不是同一个对象,尽管0 == 1为True。
Q3:None is None为什么总是True?
A:None是全局唯一的单例对象,源码中Py_None是一个全局静态变量。
Q4:和[] is []分别返回什么?
A:为True(空列表值相等),[] is []为False(每次创建新列表)。
Q5:class A: pass; a = A(); b = A(); a is b返回?
A:False,默认__eq__未定义时退化为is,但is本身不会调用__eq__。
Q6:为什么(1, 2) is (1, 2)有时为True?
A:CPython对某些简单元组会做常量折叠(constant folding)优化,但不可依赖。
Q7:'a' * 20 is 'a' * 20在解释器中可能为True?
A:是的,CPython会在编译期优化常量表达式,但运行时拼接结果不同。
Q8:如何自定义__eq__让返回自定义结果?
A:重写__eq__方法,is仍不受影响。
Q9:bool和int的比较如何实现?
A:bool的子类机制:bool.__eq__会调用int.__eq__,所以True == 1为True。
Q10:float('nan') is float('nan')返回?
A:False,因为每次创建新对象,但nan == nan也返回False(IEEE 754标准)。
性能与安全:何时该用is,何时该用
1 必须用is的场景
- 比较
None:x is None(比x == None快且明确) - 比较
True/False:x is True而非x == True(避免1 == True的误判) - 比较单例(如
NotImplemented、Ellipsis)
2 必须用的场景
- 比较数值、字符串、列表内容
- 自定义类实例的比较
- 跨类型比较(如
1 == 1.0)
3 安全警告
不要用is比较字符串,因为字符串驻留是CPython实现细节,其他解释器(如PyPy、Jython)可能行为不同,同样,不要依赖小整数范围。
写出更健壮的Python代码
理解is和的底层区别不仅是面试技巧,更是写出可预测、高性能代码的基础:
is比较身份:直接比较指针,速度快,用于单例判断- 比较值:通过方法调用链,可被重写,用于内容判断
- 源码证明:
is在指令层直接比较指针,经过tp_richcompare多态分派 - 实践原则:永远用
is None判断空值,永远用比较数值内容
记住一句口诀:相同就一样,用;如果必须是同一个对象,用is。”
本文基于CPython 3.12源码解析,所有结论均可在官方GitHub仓库中验证。
标签: 内存地址