如何从源码角度理解Python的is和=判断的底层区别

访客 源码剖析 1

从源码角度理解Python的is和判断的底层区别

目录导读

  1. 引言:一个困扰初学者的经典问题
  2. 底层机制解析:与is的Python源码实现
  3. 核心差异:值相等 vs 身份相等
  4. 源码级对比:PyObject_RichCompare vs PyObject_RichCompareBool
  5. 经典陷阱:整数缓存、字符串驻留与可变对象
  6. 实战问答:20个高频场景测试你的理解
  7. 性能与安全:何时该用is,何时该用
  8. 写出更健壮的Python代码

一个困扰初学者的经典问题

在Python面试和日常开发中,a == ba is b的区别几乎是必考题,表面上,比较“值”,is比较“身份”,但为什么5 == 5.0返回True,而5 is 5.0返回False?为什么a = 256b = 256a is bTrue,但a = 257时却为False?要彻底理解,必须深入CPython源码。


底层机制解析:与is的Python源码实现

在CPython(Python的官方解释器)中,is和的实现路径完全不同。

1 is运算符的源码路径

is对应字节码COMPARE_OP中的IS_OP,核心实现在Python/ceval.ccase 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.cPyObject_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.cobject_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/FalseNotImplemented
  • 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 bTrue,但a = 257; b = 257时,每次都会新建对象,a is bFalse

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个高频场景测试你的理解

Q10 is False返回什么?
AFalse,因为Falsebool类型对象(值为0),0int类型对象,虽然0 == FalseTrue,但它们是不同对象。

Q20 is 1返回什么?
AFalse,浮点数和整数永远不是同一个对象,尽管0 == 1True

Q3None is None为什么总是True
ANone是全局唯一的单例对象,源码中Py_None是一个全局静态变量。

Q4:和[] is []分别返回什么?
A:为True(空列表值相等),[] is []False(每次创建新列表)。

Q5class A: pass; a = A(); b = A(); a is b返回?
AFalse,默认__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仍不受影响。

Q9boolint的比较如何实现?
Abool的子类机制:bool.__eq__会调用int.__eq__,所以True == 1True

Q10float('nan') is float('nan')返回?
AFalse,因为每次创建新对象,但nan == nan也返回False(IEEE 754标准)。


性能与安全:何时该用is,何时该用

1 必须用is的场景

  • 比较Nonex is None(比x == None快且明确)
  • 比较True/Falsex is True而非x == True(避免1 == True的误判)
  • 比较单例(如NotImplementedEllipsis

2 必须用的场景

  • 比较数值、字符串、列表内容
  • 自定义类实例的比较
  • 跨类型比较(如1 == 1.0

3 安全警告

不要用is比较字符串,因为字符串驻留是CPython实现细节,其他解释器(如PyPy、Jython)可能行为不同,同样,不要依赖小整数范围。


写出更健壮的Python代码

理解is和的底层区别不仅是面试技巧,更是写出可预测、高性能代码的基础:

  1. is比较身份:直接比较指针,速度快,用于单例判断
  2. 比较值:通过方法调用链,可被重写,用于内容判断
  3. 源码证明is在指令层直接比较指针,经过tp_richcompare多态分派
  4. 实践原则:永远用is None判断空值,永远用比较数值内容

记住一句口诀:相同就一样,用;如果必须是同一个对象,用is。”


本文基于CPython 3.12源码解析,所有结论均可在官方GitHub仓库中验证。

标签: 内存地址

抱歉,评论功能暂时关闭!