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 True比if 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. == 走相等性协议
== 本质上会尝试调用对象的相等性逻辑。
常见理解顺序:
- 优先看对象类型是否实现了
__eq__ - 如果实现了,就按它的规则比较
- 如果没实现合适逻辑,最终退回默认行为
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
这能说明:
a和b共享身份,所以改一个另一个也变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" 偶尔为真,容易误导 | 值相等判断用 == |
| 对单例/哨兵对象比较 | 推荐 | 不够准确 | 用 == 可能被重载干扰 | None、Ellipsis、自定义 sentinel |
| 与拷贝的关系 | 拷贝后通常为 False | 拷贝后常常仍为 True | 搞不清浅拷贝和深拷贝的身份关系 | 分析共享引用、别名、副作用 |
| 与可变对象的关系 | 可揭示是否共享同一底层对象 | 只能说明当前内容是否一致 | 把“现在一样”误当成“永远一起变” | 排查列表、字典共享引用 bug |
| 默认行为 | 判断是否同一对象 | 若未重写 __eq__,默认常接近身份比较 | 误以为所有对象 == 都比较字段 | 自定义类要明确设计相等规则 |
| 布尔判断中的使用 | 仅在严格判断 True/False 单例时用 | 可能和 truthy/falsey 混淆 | x == True、x is True 都常被滥用 | 一般写 if x:,不是 if x is True: |
易错点
-
用
is比较普通数值、字符串、列表内容 例如:if x is 42、if s is "ok",都不应该作为值比较写法。 -
把“小整数缓存”“字符串驻留”当作语言保证 这些只是实现优化,不能作为业务逻辑依据。
-
误以为
==一定比is“更深”或“更高级” 它们不是层级关系,而是比较维度不同:身份 vs 相等性。 -
比较
None用==容易被自定义__eq__干扰,也不够语义化。 -
误把
if x is True当成“判断真值”的推荐写法 这只在你要严格判断“布尔单例本身”时才合理;大多数时候应写if x:。 -
自定义
__eq__后忽略__hash__会导致放进set、作为dict键时出现不一致行为。 -
以为“
a == b为真”就说明“改 a 会影响 b” 能不能相互影响,取决于是不是同一个对象,或者内部是否共享可变子对象。 -
把
is和in混淆is是身份比较,in是成员判断。 -
过度依赖
id()id()可以帮助理解身份,但不要把“身份”机械等同成“永远唯一不变的地址”;对象销毁后,某些实现里id可能复用。
记忆技巧
-
口诀一:
is看人,==看脸。is看是不是同一个对象;==看表现出来的值是否一样。 -
口诀二: 凡是“唯一对象”,优先
is;凡是“业务数据”,优先==。 -
口诀三:
None、单例、哨兵,用is;数字、字符串、容器内容,用==。 -
判断代码是否该用
is,问自己一句: “我关心的是不是同一个对象本身?” 如果不是,大概率就该用==。 -
拷贝场景记忆: 拷贝后通常“值还像,身份已变”。 所以常见现象是:
== True,is False。
面试速答版
is 和 == 的区别,本质上是对象身份和对象相等性的区别。
is 判断两个名字是不是绑定到同一个对象;== 判断两个对象按该类型定义的规则是否相等,通常由 __eq__ 决定。比如两个列表内容一样,== 可以是 True,但它们不是同一个列表,所以 is 是 False。
工程上最重要的结论是:比较 None 用 is None,比较普通值相等用 ==。不要因为小整数缓存或字符串驻留导致某些 is 恰好为真,就把 is 当成值比较工具,那只是解释器优化,不是可靠语义。
面试加分版
我会把 is 和 == 放到 Python 的对象模型里解释。Python 变量本质上不是“盒子装值”,而是名字绑定对象。所以 is 比较的是两个名字是否绑定到同一个对象,也就是对象身份;而 == 比较的是两个对象在该类型定义下是否“相等”,通常走 __eq__ 逻辑。
比如两个列表 [1, 2, 3] 内容相同,== 是 True,但它们是两个独立对象,所以 is 是 False。如果 b = a,那 a is b 就是 True,因为两个名字指向同一个对象。这个区别在可变对象里尤其重要,因为它直接关系到是否共享修改、副作用以及浅拷贝深拷贝的问题。
工程实践里最关键的是两条:
第一,比较 None 必须优先用 is None,因为 None 是单例,而且 == 可能被自定义 __eq__ 干扰。
第二,普通业务数据的相等性判断用 ==,比如数字、字符串、列表、字典、自定义值对象等。
另外有个高频误区:有时你会看到小整数或短字符串用 is 也是 True,这是解释器做了对象复用优化,比如小整数缓存、字符串驻留,但这是实现细节,不能写进业务逻辑。
如果面试官继续追问,我还会补充:自定义类如果重写了 __eq__,最好同时考虑 __hash__ 一致性;而在需要区分“未传参”和“显式传 None”时,通常会用自定义 sentinel 对象,再通过 is 判断,这属于真实工程里很常见的做法。