🐍Python 语言基础

Python 中 is==

面试回答

常见问法

  • is== 有什么区别?
  • 为什么判断 None 推荐用 is,不推荐 ==
  • 为什么有时候两个整数或字符串用 is 也会是 True
  • 自定义类里 == 到底比较什么?
  • 浅拷贝、深拷贝后,is== 分别会怎样?

回答

is 比较的是对象身份(identity),也就是两个名字是否绑定到同一个对象== 比较的是对象内容/值是否相等,本质上走的是对象的相等性协议,通常由 __eq__ 决定。

面试里最好不要只停留在“地址”和“值”这两个词上,而要讲清楚 Python 的对象模型:

  • Python 里变量本质上是名字绑定到对象
  • is 看的是:两个名字是不是绑到了同一个对象
  • == 看的是:这两个对象按照该类型定义的“相等规则”是否相等

最典型的例子:

a = [1, 2, 3]
b = [1, 2, 3]
c = a

print(a == b)  # True,内容相等
print(a is b)  # False,不是同一个对象

print(a == c)  # True
print(a is c)  # True,同一个对象

一句话总结:

  • is:比较“是不是同一个对象”
  • ==:比较“按该类型定义,算不算相等”

再补一句高频结论:

  • 比较 None,用 is None
  • 比较数值、字符串、列表、字典等值相等,通常用 ==
  • 不要因为某次 1000 is 1000"abc" is "abc" 恰好为 True,就把 is 当值比较用,那是实现优化现象,不是业务逻辑依据

追问

1)为什么 None 推荐用 is

因为 None 是一个单例对象,全局只有一个。判断“是不是空值对象本身”,最准确的方式就是身份比较。

x = None

print(x is None)  # True
print(x == None)  # 也常常是 True,但不推荐

不推荐 x == None 的原因有两个:

  • 语义不够准确:你要判断的是“是不是那个唯一的 None 对象”
  • == 可能被自定义类重载,导致行为不可靠
class Weird:
    def __eq__(self, other):
        return True

w = Weird()
print(w == None)   # True,误导
print(w is None)   # False,准确

2)== 是谁决定的?

由对象的相等性逻辑决定,通常是 __eq__

  • 如果类没有自定义 __eq__,默认继承自 object
  • 默认情况下,object.__eq__ 的行为接近“身份相等才算相等”
  • 一旦重写了 __eq__== 就不再是“是不是同一个对象”,而是“你定义的规则是否相等”
class Person:
    def __init__(self, name):
        self.name = name

    def __eq__(self, other):
        return isinstance(other, Person) and self.name == other.name

p1 = Person("Alice")
p2 = Person("Alice")

print(p1 == p2)  # True,按 name 比较
print(p1 is p2)  # False,不是同一个对象

3)为什么有时候 is 比较整数、字符串也会是 True

这是解释器优化导致的对象复用现象,比如:

  • 小整数缓存
  • 字符串驻留(interning)
  • 编译期常量折叠

但这些都属于实现细节,不是你该依赖的业务语义。

a = 256
b = 256
print(a is b)  # 可能 True

x = 1000
y = 1000
print(x is y)  # 结果不要依赖,解释器/上下文可能不同

面试里最好强调:

这些现象说明“某些对象被复用”,但不能反推“普通值比较应该用 is”。


4)浅拷贝、深拷贝和 is / == 的关系?

这是非常适合加分的追问。

import copy

a = [[1, 2], [3, 4]]
b = copy.copy(a)
c = copy.deepcopy(a)

print(a == b, a is b)  # True False
print(a == c, a is c)  # True False

print(a[0] is b[0])    # True,浅拷贝内部对象共享
print(a[0] is c[0])    # False,深拷贝内部对象也复制

说明:

  • 拷贝后,一般 is 都是 False,因为是新对象
  • == 往往仍然是 True,因为内容一样
  • 浅拷贝和深拷贝的差异,核心就体现在内部子对象是否共享身份

这能很好体现你对对象模型 + 可变性 + 拷贝语义的理解。


5)布尔值到底该用 is 还是 ==

要分场景回答:

  • 如果你在判断某个表达式“是否为真”,通常写 if x:,而不是 if x is True:
  • 如果你确实要判断“这个值是不是布尔单例 True/False 本身”,可以用 is
x = 1
print(bool(x))        # True
print(x == True)      # True
print(x is True)      # False

面试里这句话很重要:

if x is Trueif x: 更严格,它只接受真正的 True 对象,不接受一般 truthy 值。

所以工程里:

  • 判断真值:if x:
  • 判断是否为空对象:if x is None:
  • 只有在需要区分“True 本体”和“truthy 值”时,才用 is True

6)有没有 == 为假,但 is 为真?

有。

最典型的是 NaN

x = float('nan')
print(x is x)  # True,同一个对象
print(x == x)  # False,NaN 按 IEEE 规则不等于自己

这是很好的加分点,说明你知道:

  • is== 是两个完全不同维度
  • “同一个对象”并不一定“值相等”

原理展开

1. Python 里变量不是盒子,而是名字绑定

很多人把变量理解成“变量里装着值”,这在 Python 面试里不够准确。更好的表述是:

变量名只是一个标签,绑定到对象上。

a = [1, 2, 3]
b = a

这里不是“把 a 复制给 b”,而是:

  • 先创建一个列表对象
  • a 绑定到它
  • b 也绑定到同一个对象

所以:

print(a is b)  # True

这也是 is 的本质基础。


2. is 比较的是对象身份,不是“值”

is 不会调用 __eq__,不会看内容,它只看两边是不是同一个对象。

a = [1, 2]
b = [1, 2]
c = a

print(a is b)  # False
print(a is c)  # True

在 CPython 里,你经常会看到大家用 id() 辅助理解:

print(id(a), id(b), id(c))

但面试里最好说得更严谨一点:

  • id() 可以帮助你观察对象身份
  • 但语言层面保证的是“对象身份”这个概念
  • 不要把“身份”简单等同于“内存地址”去死记,因为那是具体实现视角

3. == 走相等性协议

== 本质上会尝试调用对象的相等性逻辑。

常见理解顺序:

  1. 优先看对象类型是否实现了 __eq__
  2. 如果实现了,就按它的规则比较
  3. 如果没实现合适逻辑,最终退回默认行为
class A:
    def __init__(self, x):
        self.x = x

class B:
    def __init__(self, x):
        self.x = x

a1 = A(1)
a2 = A(1)

print(a1 == a2)  # 默认通常是 False,因为没有自定义 __eq__

自定义后:

class A:
    def __init__(self, x):
        self.x = x

    def __eq__(self, other):
        if not isinstance(other, A):
            return NotImplemented
        return self.x == other.x

这里顺手带出一个更专业的点:

  • 对不支持比较的类型,__eq__ 应该优先返回 NotImplemented
  • 而不是直接瞎返回 False
  • 这样 Python 才有机会尝试反向比较或采用更合理的默认处理

这是比“会写 __eq__”更高级的回答。


4. 容器类型的 == 往往是递归比较内容

比如:

  • list:按位置逐项比较
  • tuple:按位置逐项比较
  • dict:比较键和值
  • set:比较元素集合是否相同
print([1, 2] == [1, 2])                # True
print((1, 2) == (1, 2))                # True
print({"a": 1} == {"a": 1})            # True
print({1, 2} == {2, 1})                # True

所以你会看到:

  • 容器对象本身不是同一个对象
  • 但内容按规则相等,所以 == 为真

5. 可变对象更能体现 is== 的差别

不可变对象有时会因为优化“看起来像同一个对象”,容易让初学者误判;可变对象更适合说明本质。

a = [1, 2]
b = a
c = [1, 2]

a.append(3)

print(b)         # [1, 2, 3]
print(c)         # [1, 2]

print(a is b)    # True
print(a == c)    # False

这能说明:

  • ab 共享身份,所以改一个另一个也变
  • c 只是最初值相同,但不是同一对象

6. 为什么解释器优化不能当语言规则?

因为下面这些都可能影响 is 的结果:

  • 解释器实现(CPython、PyPy 等)
  • 运行环境
  • 编译优化
  • 是否在同一代码块中创建
  • 常量是否被复用

所以工程里应坚持一个原则:

除了比较 None、单例对象、自定义哨兵对象,is 不应用来表达“值相等”。


7. 工程实践里怎么用?

场景一:判断缺省值/哨兵值

_sentinel = object()

def func(x=_sentinel):
    if x is _sentinel:
        print("调用方没有传参")
    elif x is None:
        print("调用方显式传了 None")
    else:
        print("传了实际值")

这是非常好的工程实践例子。因为:

  • None 有时是合法业务值
  • 需要另一个“唯一对象”区分“未传参”
  • 这类场景必须用 is

场景二:数据对象内容比较

user1 = {"name": "Tom", "age": 18}
user2 = {"name": "Tom", "age": 18}

print(user1 == user2)  # True

业务里通常关心的是“数据是否一致”,而不是“是不是同一个字典对象”,所以用 ==


场景三:缓存、单例、对象池

当业务确实关心“是不是同一个实例”,比如:

  • 单例对象
  • 缓存返回的同一实例
  • 哨兵对象
  • ORM/依赖注入容器里对象身份语义

这时才应该用 is


场景四:自定义对象的相等性设计

如果类重写了 __eq__,还要考虑 __hash__ 一致性问题。

class User:
    def __init__(self, uid):
        self.uid = uid

    def __eq__(self, other):
        if not isinstance(other, User):
            return NotImplemented
        return self.uid == other.uid

如果对象要放进 set 或当作 dict 键,就要确保:

  • 相等的对象,哈希也应相等
  • 否则容器行为会出问题

这是回答 == 时很好的延展点。


对比总结

对比维度is==面试易混点适用场景
比较目标对象身份对象相等性/值很多人把两者都理解成“比较值”明确区分“同一个对象”与“内容相等”
是否可重载不可由普通类重载可通过 __eq__ 自定义以为 is 也会走魔术方法== 适合业务语义比较
是否受解释器优化影响结果可能受对象复用影响语义更稳定把小整数缓存当语言保证普通值比较优先用 ==
None 的比较推荐不推荐以为两者都一样就随便用x is None
对数字/字符串/容器内容比较不推荐推荐1 is 1"a" is "a" 偶尔为真,容易误导值相等判断用 ==
对单例/哨兵对象比较推荐不够准确== 可能被重载干扰NoneEllipsis、自定义 sentinel
与拷贝的关系拷贝后通常为 False拷贝后常常仍为 True搞不清浅拷贝和深拷贝的身份关系分析共享引用、别名、副作用
与可变对象的关系可揭示是否共享同一底层对象只能说明当前内容是否一致把“现在一样”误当成“永远一起变”排查列表、字典共享引用 bug
默认行为判断是否同一对象若未重写 __eq__,默认常接近身份比较误以为所有对象 == 都比较字段自定义类要明确设计相等规则
布尔判断中的使用仅在严格判断 True/False 单例时用可能和 truthy/falsey 混淆x == Truex is True 都常被滥用一般写 if x:,不是 if x is True:

易错点

  • is 比较普通数值、字符串、列表内容 例如:if x is 42if s is "ok",都不应该作为值比较写法。

  • 把“小整数缓存”“字符串驻留”当作语言保证 这些只是实现优化,不能作为业务逻辑依据。

  • 误以为 == 一定比 is “更深”或“更高级” 它们不是层级关系,而是比较维度不同:身份 vs 相等性。

  • 比较 None== 容易被自定义 __eq__ 干扰,也不够语义化。

  • 误把 if x is True 当成“判断真值”的推荐写法 这只在你要严格判断“布尔单例本身”时才合理;大多数时候应写 if x:

  • 自定义 __eq__ 后忽略 __hash__ 会导致放进 set、作为 dict 键时出现不一致行为。

  • 以为“a == b 为真”就说明“改 a 会影响 b” 能不能相互影响,取决于是不是同一个对象,或者内部是否共享可变子对象。

  • isin 混淆 is 是身份比较,in 是成员判断。

  • 过度依赖 id() id() 可以帮助理解身份,但不要把“身份”机械等同成“永远唯一不变的地址”;对象销毁后,某些实现里 id 可能复用。


记忆技巧

  • 口诀一: is 看人,== 看脸。 is 看是不是同一个对象;== 看表现出来的值是否一样。

  • 口诀二: 凡是“唯一对象”,优先 is;凡是“业务数据”,优先 ==

  • 口诀三: None、单例、哨兵,用 is;数字、字符串、容器内容,用 ==

  • 判断代码是否该用 is,问自己一句: “我关心的是不是同一个对象本身?” 如果不是,大概率就该用 ==

  • 拷贝场景记忆: 拷贝后通常“值还像,身份已变”。 所以常见现象是:== Trueis False


面试速答版

is== 的区别,本质上是对象身份对象相等性的区别。 is 判断两个名字是不是绑定到同一个对象;== 判断两个对象按该类型定义的规则是否相等,通常由 __eq__ 决定。比如两个列表内容一样,== 可以是 True,但它们不是同一个列表,所以 isFalse。 工程上最重要的结论是:比较 Noneis None,比较普通值相等用 ==。不要因为小整数缓存或字符串驻留导致某些 is 恰好为真,就把 is 当成值比较工具,那只是解释器优化,不是可靠语义。


面试加分版

我会把 is== 放到 Python 的对象模型里解释。Python 变量本质上不是“盒子装值”,而是名字绑定对象。所以 is 比较的是两个名字是否绑定到同一个对象,也就是对象身份;而 == 比较的是两个对象在该类型定义下是否“相等”,通常走 __eq__ 逻辑。

比如两个列表 [1, 2, 3] 内容相同,==True,但它们是两个独立对象,所以 isFalse。如果 b = a,那 a is b 就是 True,因为两个名字指向同一个对象。这个区别在可变对象里尤其重要,因为它直接关系到是否共享修改、副作用以及浅拷贝深拷贝的问题。

工程实践里最关键的是两条: 第一,比较 None 必须优先用 is None,因为 None 是单例,而且 == 可能被自定义 __eq__ 干扰。 第二,普通业务数据的相等性判断用 ==,比如数字、字符串、列表、字典、自定义值对象等。

另外有个高频误区:有时你会看到小整数或短字符串用 is 也是 True,这是解释器做了对象复用优化,比如小整数缓存、字符串驻留,但这是实现细节,不能写进业务逻辑。 如果面试官继续追问,我还会补充:自定义类如果重写了 __eq__,最好同时考虑 __hash__ 一致性;而在需要区分“未传参”和“显式传 None”时,通常会用自定义 sentinel 对象,再通过 is 判断,这属于真实工程里很常见的做法。