身份与成员运算符
身份运算符 is 和 is not 用于判断两个名字是否引用内存中的同一个对象;成员运算符 in 和 not in 用于判断某个值是否存在于容器或可迭代对象中。这两类运算符解决的问题与 == 完全不同,理解它们的差异是避免隐蔽 bug 的关键。
is 与 is not
is 比较的是对象的身份(identity),即它们在内存中的地址。两个对象即使值完全相同,如果存储在不同的内存位置,is 也返回 False。is not 是 is 的否定形式。
>>> a = [1, 2, 3]
>>> b = [1, 2, 3]
>>> a == b
True # 值相等
>>> a is b
False # 不是同一个对象
>>> id(a) == id(b)
False # id() 返回对象身份,is 本质上比较 id
is 的核心使用场景是判断 None。由于 None 在 Python 中是单例(全局只有一个 None 对象),is None 是判断某个值是否为 None 的标准且最可靠的方式。
>>> x = None
>>> x is None
True
>>> x is not None
False
# 函数参数默认值检查
def process(data=None):
if data is None:
data = []
# ...
为什么不用 == 判断 None?因为 == 可以被自定义类重载,而 is 不能。某个对象可能重载了 __eq__ 使其对 None 返回 True,但 is None 永远不会被欺骗。
class Weird:
def __eq__(self, other):
return True # 对任何比较都返回 True
>>> w = Weird()
>>> w == None
True # 被欺骗了
>>> w is None
False # 安全的判断
== 与 is 的区别
| 运算符 | 比较内容 | 适用场景 |
|---|---|---|
== | 值相等(调用 __eq__) | 数字、字符串内容、数据结构 |
is | 身份相同(内存地址) | None 判断、单例对象、同一对象检测 |
初学者常犯的错误是用 is 比较字符串或数字:
>>> a = 1000
>>> b = 1000
>>> a == b
True
>>> a is b
False # 大整数不是单例
>>> a = "hello world"
>>> b = "hello world"
>>> a == b
True
>>> a is b
False # 运行时构造的字符串通常不是同一对象
小整数缓存
Python 为了优化性能,对 -5 到 256 范围内的小整数进行了缓存,这些数字在任何地方使用都是同一个对象。
>>> a = 256
>>> b = 256
>>> a is b
True # 在缓存范围内
>>> a = 257
>>> b = 257
>>> a is b
False # 超出缓存范围,创建了两个对象
>>> a = -5
>>> b = -5
>>> a is b
True
>>> a = -6
>>> b = -6
>>> a is b
False
字符串也有类似的驻留(intern)机制,但仅限于编译期确定的标识符风格字符串,不应依赖此行为:
>>> a = "python"
>>> b = "python"
>>> a is b
True # 标识符风格字符串可能被驻留
>>> a = "hello world!"
>>> b = "hello world!"
>>> a is b
False # 含空格和标点,通常不驻留
依赖 is 进行值比较是不可靠的编程习惯,应始终使用 ==。
in 与 not in
in 判断左侧操作数是否是右侧容器或可迭代对象的成员,返回 True 或 False。not in 是其否定形式。
>>> 3 in [1, 2, 3, 4]
True
>>> 5 in [1, 2, 3, 4]
False
>>> "a" in "banana"
True
>>> "z" not in "banana"
True
不同容器类型的 in 操作效率差异显著:
- 列表、元组:线性扫描,时间复杂度 O(n)
- 集合、字典(判断键):哈希查找,时间复杂度 O(1)
- 字符串:子串匹配,使用高效的 C 级算法
>>> 3 in {1, 2, 3, 4} # 集合,O(1)
True
>>> "name" in {"name": "Alice", "age": 25} # 字典判断键,O(1)
True
>>> "Alice" in {"name": "Alice", "age": 25} # 不判断值!
False
字典的 in 只检查键,不检查值。如果需要判断值是否存在,应使用 .values():
>>> "Alice" in {"name": "Alice", "age": 25}.values()
True
可迭代对象的成员检查
in 运算符依赖对象的 __contains__ 方法。如果对象没有实现该方法,Python 会尝试迭代对象来查找匹配项。
>>> 2 in range(5)
True # range 实现了高效的 __contains__
>>> (1, 2) in [(1, 2), (3, 4)]
True # 元组作为列表元素
>>> 2 in (x for x in range(10) if x % 2 == 0)
True # 生成器表达式,逐元素检查
自定义类可以通过实现 __contains__ 来支持 in 运算:
class Range:
def __init__(self, start, end):
self.start = start
self.end = end
def __contains__(self, value):
return self.start <= value < self.end
>>> r = Range(10, 20)
>>> 15 in r
True
>>> 20 in r
False # 右开区间
嵌套容器的成员检查
in 对嵌套容器只检查顶层成员,不进行递归搜索:
>>> 3 in [1, [2, 3], 4]
False # 3 不在顶层,[2, 3] 才是顶层元素
>>> [2, 3] in [1, [2, 3], 4]
True
如果需要递归查找,应自行实现或使用专门的库函数。
身份运算符的边界情况
对于布尔值,True 和 False 是 1 和 0 的单例,因此:
>>> True is 1
False # True 是 bool 类型,1 是 int 类型,身份不同
>>> True == 1
True # 但值相等
>>> False is 0
False
在 Python 3 中,bool 是 int 的子类,但 True 和 1 仍是不同对象。
身份与成员运算符解决的问题领域与 == 截然不同。is 回答"是否是同一个东西",in 回答"是否在其中",而 == 回答"是否相等"。用 is 判断 None、用 == 判断值、用 in 判断成员关系,是 Python 编程中应当遵循的基本约定。