Python基础指南之is与==的区别及使用场景详解
作者:星河耀银海
一、开篇:一个让无数Python初学者困惑的问题
先看一段代码,猜猜输出什么:
a = [1, 2, 3] b = [1, 2, 3] print(a == b) # ? print(a is b) # ?
如果你的答案是"两个都输出True",那这篇文章就是为你准备的。正确答案是:
print(a == b) # True —— 值相等 print(a is b) # False —— 但不是同一个对象!
再来看另一段让人困惑的代码:
a = 256 b = 256 print(a == b) # True print(a is b) # True —— 咦?这次is也是True? a = 257 b = 257 print(a == b) # True print(a is b) # False —— 又变成False了?!
这种看似不一致的行为,根源就在于==和is的本质区别——一个比较值,一个比较身份。今天这篇文章,我就带你深入理解这两个操作符的底层原理和使用场景,让你以后再也不会在这上面踩坑。
二、核心区别:== 比的是值,is 比的是身份
2.1 一句话总结
- == 比较两个对象的值是否相等(调用 __eq__ 方法)
- is 比较两个对象是否是同一个对象(比较 id())
用生活中的类比:
- == 相当于:这两张100元钞票的购买力相同吗?(比价值)
- is 相当于:这是同一张钞票吗?(比身份)
2.2 用id()理解is
is操作符本质上就是比较两个对象的id()是否相同:
a = [1, 2, 3]
b = [1, 2, 3]
c = a
print(f'id(a) = {id(a)}')
print(f'id(b) = {id(b)}')
print(f'id(c) = {id(c)}')
# a is c → True(同一个对象,id相同)
print(f'a is c: {a is c}') # True
print(f'id(a) == id(c): {id(a) == id(c)}') # True
# a is b → False(虽然值相同,但id不同)
print(f'a is b: {a is b}') # False
print(f'id(a) == id(b): {id(a) == id(b)}') # False
# 但它们的值相等
print(f'a == b: {a == b}') # True
2.3 图解:值和身份的区别
a = [1, 2, 3]
b = [1, 2, 3]
c = a
内存中的状态:
- a ──→ [1, 2, 3] ←── c
- b ──→ [1, 2, 3] (另一个独立的列表)
a is c → True (指向同一个对象)
a is b → False (指向不同的对象)
a == b → True (两个对象的值相同)
三、深入理解==
3.1 == 调用的是__eq__方法
当你写a == b时,Python实际上调用的是a.__eq__(b):
# a == b 等价于 a.__eq__(b)
a = [1, 2, 3]
b = [1, 2, 3]
# 这两种写法等价
print(a == b) # True
print(a.__eq__(b)) # True
# 不同类型的对象有不同的__eq__实现
print([1, 2] == [1, 2]) # True——列表逐元素比较
print((1, 2) == (1, 2)) # True——元组逐元素比较
print({'a': 1} == {'a': 1}) # True——字典逐键值比较
print({1, 2} == {2, 1}) # True——集合内容相同,顺序无关
# 自定义类的__eq__
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __eq__(self, other):
if not isinstance(other, Person):
return False
return self.name == other.name and self.age == other.age
p1 = Person('小明', 25)
p2 = Person('小明', 25)
p3 = Person('小红', 30)
print(p1 == p2) # True——我们定义了__eq__,比较name和age
print(p1 == p3) # False
3.2 没有自定义__eq__时的默认行为
# 如果类没有定义__eq__,默认继承object的__eq__
# object.__eq__默认比较的就是is(身份)
class Simple:
def __init__(self, value):
self.value = value
s1 = Simple(10)
s2 = Simple(10)
s3 = s1
print(s1 == s2) # False——没有自定义__eq__,默认比较身份
print(s1 == s3) # True——同一个对象
print(s1 is s2) # False
print(s1 is s3) # True
3.3 ==的比较逻辑
Python在执行a == b时的实际流程:
Python的==比较流程:
1. 先尝试调用 a.__eq__(b)
2. 如果返回 NotImplemented,尝试调用 b.__eq__(a)
3. 如果都返回 NotImplemented,回退到比较 id(a) == id(b)(即is)
# 实验:观察NotImplemented的行为
class AlwaysNotImplemented:
def __eq__(self, other):
return NotImplemented
class AlwaysTrueEquals:
def __eq__(self, other):
return True
a = AlwaysNotImplemented()
b = AlwaysTrueEquals()
# a.__eq__(b)返回NotImplemented,于是尝试b.__eq__(a)
# b.__eq__(a)返回True
print(a == b) # True四、深入理解is
4.1 is比较的是对象身份
is操作符不能被重载——它永远比较对象身份
你无法通过定义__eq__或任何特殊方法来改变is的行为
class MyClass:
def __eq__(self, other):
return True # 让==始终返回True
# 没有__is__方法!is的行为无法改变
obj1 = MyClass()
obj2 = MyClass()
print(obj1 == obj2) # True——__eq__被重载了
print(obj1 is obj2) # False——is不受影响,永远比较身份4.2 is的等价写法
a is b 和 id(a) == id(b) 在效果上等价
但is是原子操作,更高效也更安全
a = [1, 2, 3] b = a print(a is b) # True print(id(a) == id(b)) # True
但请注意:不推荐用id() == id()替代is
- 原因1:id()在极短时间内可能被复用(对象销毁后新对象可能获得相同id)
- 原因2:is是语法级别的操作,比函数调用更快
- 原因3:is更符合Python的编码风格
反例——这个极短时间差里可能出问题
temp_a = some_object temp_b = other_object result = (id(temp_a) == id(temp_b)) # 不如直接用 temp_a is temp_b
五、Python的整数缓存和字符串驻留
5.1 小整数缓存 [-5, 256]
这是Python初学者最容易困惑的地方。为什么256 is 256是True,257 is 257却是False?
# Python在启动时会预创建-5到256之间的所有整数对象 # 这些整数会被缓存和复用 # 范围内的整数——被缓存 a = 256 b = 256 print(a is b) # True——两个变量都指向缓存中的同一个256对象 a = -5 b = -5 print(a is b) # True——同样被缓存 # 范围外的整数——每次创建新对象 a = 257 b = 257 print(a is b) # False——每次写257都创建新对象 a = -6 b = -6 print(a is b) # False——-6不在缓存范围内 # 💡 但注意:同一行赋值的相同整数也可能共享 a = 257; b = 257 print(a is b) # 可能True!因为解释器在同一行可能会优化 # 这是编译器优化行为,不要依赖它
5.2 字符串驻留(String Interning)
# Python对某些字符串也会做驻留(intern)——缓存和复用
# 简单字符串——通常会被驻留
a = "hello"
b = "hello"
print(a is b) # True——字符串被驻留了
# 包含空格的字符串——也可能被驻留
a = "hello world"
b = "hello world"
print(a is b) # True——通常也会被驻留
# 但动态拼接的字符串——不一定被驻留
a = "hello"
b = "world"
c = a + b
d = "helloworld"
print(c is d) # 可能False——动态拼接的不一定被驻留
print(c == d) # True——值相等
# 用sys.intern()手动驻留
import sys
a = sys.intern("hello world " + "!")
b = sys.intern("hello world " + "!")
print(a is b) # True——手动驻留后就是同一个对象
# ⚠️ 字符串驻留规则取决于Python实现细节
# 不要在生产代码中依赖字符串is的比较!
# 始终用==来比较字符串的值
5.3 其他类型的缓存行为
# 空元组——被缓存 a = () b = () print(a is b) # True——空元组是单例 # 小元组——可能被缓存(取决于实现) a = (1, 2, 3) b = (1, 2, 3) print(a is b) # 可能True也可能False——不要依赖 # None——单例 a = None b = None print(a is b) # True——None永远是同一个对象 # True和False——单例 a = True b = True print(a is b) # True # 小列表和字典——不会被缓存 a = [] b = [] print(a is b) # False——每次创建新列表
六、什么时候用is?
6.1 黄金法则:和None比较时永远用is
# ✅ 推荐——用is比较None
if x is None:
print('x是None')
if result is not None:
print('有结果')
# ❌ 不推荐——用==比较None
if x == None: # 理论上可以,但是...
print('x是None')
# 为什么is更好?
# 1. None是单例——整个Python进程中只有一个None对象,is是最精确的检查
# 2. is比==更快——不需要调用__eq__方法
# 3. is不会被重载——==可能被对象的__eq__重载,产生意外行为
# 4. PEP 8明确推荐:Comparisons to singletons like None should always be done with is or is not
# 意外行为的例子:
class TrickyNone:
def __eq__(self, other):
return True # 和任何东西比较都返回True
x = TrickyNone()
print(x == None) # True!——但x显然不是None
print(x is None) # False——is正确判断
6.2 其他适合用is的场景
# 1. 和True/False比较——通常不需要显式比较
# ✅ 直接用布尔上下文
if some_value: # 而不是 if some_value is True:
pass
# 特殊情况下用is(如需要区分True和1)
flag = True
print(flag is True) # True
print(flag == 1) # 也是True(因为True==1)
# 2. 检查对象类型
if type(obj) is int: # 但通常用 isinstance(obj, int) 更好
pass
# 3. 检查是否是同一个哨兵对象
SENTINEL = object() # 创建唯一的哨兵对象
def search(data, target, default=SENTINEL):
result = data.get(target, SENTINEL)
if result is SENTINEL: # 用is检查是否返回了默认值
return '未找到'
return result
# 4. 检查空序列(空元组是单例)
a = ()
b = ()
print(a is b) # True——但通常用 len(a) == 0 或 not a 更好
6.3 什么时候不应该用is
# ❌ 不要用is比较数值
a = 1000
b = 1000
if a is b: # 危险!可能False
pass
# ❌ 不要用is比较字符串(除非你知道自己在做什么)
name = input("输入名字: ")
if name is "admin": # 危险!几乎肯定是False
pass
# ❌ 不要用is比较列表、字典、集合的内容
lst1 = [1, 2, 3]
lst2 = [1, 2, 3]
if lst1 is lst2: # 危险!只要不是同一个对象就是False
pass
# ✅ 这些情况用==
if a == b: # 比较值
if name == "admin": # 比较字符串
if lst1 == lst2: # 比较列表
七、实战场景
7.1 哨兵对象的经典用法
# 哨兵对象(Sentinel)——用is来判断"未设置"或"未找到"
# 为什么不用None?因为None可能是合法的返回值
# 创建唯一的哨兵对象
_MISSING = object()
_DELETED = object()
class Cache:
def __init__(self):
self._data = {}
def get(self, key, default=_MISSING):
"""获取缓存值。可以区分"值为None"和"键不存在"。"""
if key in self._data:
value = self._data[key]
if value is _DELETED:
# 键被标记为删除
if default is _MISSING:
raise KeyError(key)
return default
return value
else:
if default is _MISSING:
raise KeyError(key)
return default
def delete(self, key):
"""标记删除(而不是真的删除,保留墓碑)"""
self._data[key] = _DELETED
def set(self, key, value):
"""设置值——None也是合法的值"""
self._data[key] = value
# 使用示例
cache = Cache()
cache.set('name', None) # None是合法值
cache.set('age', 25)
cache.delete('age')
print(cache.get('name')) # None——合法的存储值
print(cache.get('age', 'N/A')) # 'N/A'——被删除了
print(cache.get('email', 'N/A')) # 'N/A'——不存在
# 如果没有哨兵对象,怎么区分"值为None"和"键不存在"?
# 用None做默认值就无法区分了!
7.2 单例模式中的is
# 单例模式——全局只有一个实例
# 用is来验证单例特性
class AppConfig:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self):
if self._initialized:
return
self.config = {}
self._initialized = True
def load(self, **kwargs):
self.config.update(kwargs)
# 验证单例
config1 = AppConfig()
config2 = AppConfig()
print(config1 is config2) # True——同一个实例
# 整个应用中只有一个AppConfig实例
config1.load(host='localhost', port=8080)
print(config2.config) # {'host': 'localhost', 'port': 8080}
# config2看到了config1的设置——因为它们是同一个对象
7.3 循环链表检测中的is
# 检测链表中是否有环——is的经典算法应用
class Node:
def __init__(self, value):
self.value = value
self.next = None
def has_cycle(head):
"""
用快慢指针检测链表环。
如果存在环,快慢指针最终会指向同一个节点(is判断)。
"""
if head is None:
return False
slow = head
fast = head
while fast is not None and fast.next is not None:
slow = slow.next
fast = fast.next.next
if slow is fast: # 关键!用is判断是否指向同一个节点
return True
return False
# 创建带环的链表
n1 = Node(1)
n2 = Node(2)
n3 = Node(3)
n4 = Node(4)
n1.next = n2
n2.next = n3
n3.next = n4
n4.next = n2 # 环!n4指向n2
print(f'有环: {has_cycle(n1)}') # True
# 为什么这里必须用is而不是==?
# 因为我们关心的是"是否指向同一个节点对象"
# 即使两个节点的value相同,它们也可能是不同节点
7.4 缓存装饰器中的is
# 用is判断缓存命中
class CacheDecorator:
def __init__(self):
self._cache = {}
self._NOT_CACHED = object() # 哨兵
def cached_call(self, func, *args):
"""带缓存——用sentinel区分'结果为None'和'未缓存'"""
key = (func.__name__, args)
result = self._cache.get(key, self._NOT_CACHED)
if result is self._NOT_CACHED: # is哨兵
result = func(*args)
self._cache[key] = result
print(f' [计算] {key} → {result}')
else:
print(f' [缓存命中] {key} → {result}')
return result
def expensive_computation(x, y):
"""模拟耗时计算"""
import time
time.sleep(0.5) # 模拟
return x * y + x + y
cache = CacheDecorator()
print('第一次调用:')
cache.cached_call(expensive_computation, 10, 20)
print('\n第二次调用(相同参数):')
cache.cached_call(expensive_computation, 10, 20)
print('\n第三次调用(不同参数):')
cache.cached_call(expensive_computation, 5, 8)
八、常见陷阱和注意事项
8.1 陷阱一:用is比较整数值
# ❌ 最常见的错误——依赖小整数缓存
def check_status(code):
if code is 200: # 危险!
return 'OK'
elif code is 404: # 危险!
return 'Not Found'
return 'Unknown'
# 在交互式环境或某些情况下,200和404在小整数范围(-5~256)内,is可能为True
# 但这是不可靠的实现细节!
# ✅ 正确方式
def check_status(code):
if code == 200:
return 'OK'
elif code == 404:
return 'Not Found'
return 'Unknown'
8.2 陷阱二:用is比较字符串
# ❌ 错误——依赖字符串驻留
def authenticate(username):
if username is 'admin': # 危险!
return True
return False
# 从文件/网络/数据库读取的字符串不会自动驻留
# username = 'admin' # 在代码中直接写的字符串可能被驻留,is可能为True
# username = 'adm' + 'in' # 动态拼接的可能不驻留,is可能为False
# ✅ 正确方式
def authenticate(username):
if username == 'admin':
return True
return False
8.3 陷阱三:is not 和 not … is 有细微差别
x = None # 这两种写法在功能上等价 print(x is not None) # True——推荐的写法(PEP 8) print(not (x is None)) # True——等价,但可读性差 # ⚠️ 千万不要写成: # print(x is (not None)) # 这完全没有意义!not None是True
8.4 陷阱四:nan的比较
import math
# nan(Not a Number)的特殊性
nan = float('nan')
# nan == nan 是 False!(IEEE 754规范)
print(nan == nan) # False——nan不等于任何值,包括自己
# nan is nan 是 True(同一个对象)
print(nan is nan) # True
# 正确检查nan的方式
print(math.isnan(nan)) # True
# ⚠️ 所以如果你想检查一个值是不是nan:
# ❌ if x == float('nan'): ——永远False
# ✅ if math.isnan(x):
8.5 陷阱五:可变对象的is和==时序
# ==的结果可能随时间变化(对可变对象)
# is的结果不会变(对象身份不变)
lst1 = [1, 2, 3]
lst2 = [1, 2, 3]
lst3 = lst1
print(f'初始: lst1 == lst2: {lst1 == lst2}') # True
print(f'初始: lst1 is lst2: {lst1 is lst2}') # False
print(f'初始: lst1 is lst3: {lst1 is lst3}') # True
# lst1被修改了
lst1.append(4)
# ==的结果变了(值变了)
print(f'修改后: lst1 == lst2: {lst1 == lst2}') # False——值不相等了
# is的结果没变(身份没变)
print(f'修改后: lst1 is lst2: {lst1 is lst2}') # False——还是不同对象
print(f'修改后: lst1 is lst3: {lst1 is lst3}') # True——还是同一个对象
九、速查表
| 场景 | 推荐 | 说明 |
|---|---|---|
| 和None比较 | x is None | PEP 8推荐,最快最安全 |
| 比较数值 | == | 不要依赖整数缓存 |
| 比较字符串 | == | 不要依赖字符串驻留 |
| 比较列表/字典/集合 | == | 比的是内容 |
| 单例模式验证 | is | 判断是否是同一个实例 |
| 哨兵对象检测 | is | 区分"值为None"和"未设置" |
| 类型检查 | isinstance() | 比type() is更好 |
| 布尔值检查 | 直接用if x: | 比if x is True:好 |
十、本篇小结
==和is的区别是Python基础中的"高频面试考点+日常开发刚需":
==(相等性比较):
- 比较两个对象的值是否相等
- 调用
__eq__方法,可以被自定义类重载 - 对可变对象,结果可能随时间变化
- 适用场景:比较数值、字符串、列表内容、字典内容
is(身份比较):
- 比较两个对象是否同一个对象(id是否相同)
- 不能被重载,永远比较对象身份
- 对同一个对象,结果永远不变
- 适用场景:和None比较、哨兵对象、单例验证
关键记忆点:
a is b等价于id(a) == id(b)- 小整数缓存 [-5, 256] 和字符串驻留是"实现细节",不要在生产代码中依赖
- 和None比较永远用
is而不是== - 比较内容用
==,比较身份用is
搞懂了is和==,下一篇我们来学一个紧密相关的主题——None对象与空值判断的正确姿势。None是Python中最特殊的单例对象之一,正确理解和处理它,能让你的代码少很多Bug。
到此这篇关于Python基础指南之is与==的区别及使用场景详解的文章就介绍到这了,更多相关Python is与==内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
