Python基础:成员运算符in与身份运算符is

一、开篇:看似简单实则大有门道
in和is,这两个Python关键字几乎是日常编码中使用频率最高的运算符之二。你以为你已经完全掌握了它们?先来做个小测试:
# 测试1:猜猜输出什么?
print(1 in [1, 2, 3]) # ?
print("ab" in "abc") # ?
print("name" in {"name": "张三"}) # ?
# 测试2:这些呢?
a = [1, 2, 3]
b = [1, 2, 3]
print(a is b) # ?
print(a is not b) # ?
# 测试3:有点难度了
x = 256
y = 256
print(x is y) # ?
x = 257
y = 257
print(x is y) # ?
# 测试4:成员运算符的效率问题
import timeit
# 在列表中查找 vs 在集合中查找,哪个快?
如果你对其中某些答案不确定,那就跟着我一起深入探究吧。💡 今天这篇文章,我们不仅讲用法,更要讲原理、讲陷阱、讲最佳实践。
二、成员运算符 in 与 not in
2.1 基本用法
# in:判断元素是否在容器中
# not in:判断元素是否不在容器中
# 字符串
print("Py" in "Python") # True —— 子串存在
print("py" in "Python") # False —— 大小写敏感
print("X" not in "Python") # True
# 列表
fruits = ["苹果", "香蕉", "橘子"]
print("苹果" in fruits) # True
print("西瓜" not in fruits) # True
# 元组
numbers = (1, 2, 3, 4, 5)
print(3 in numbers) # True
print(10 in numbers) # False
# 字典 —— 检查的是键,不是值!
user = {"name": "张三", "age": 25, "city": "北京"}
print("name" in user) # True —— 键存在
print("张三" in user) # False —— 不检查值!
print("张三" in user.values()) # True —— 要检查值得用.values()
# 集合
valid_colors = {"red", "green", "blue"}
print("red" in valid_colors) # True
print("yellow" in valid_colors) # False
2.2 in 的底层机制:调用 contains
当你写x in container时,Python实际上调用的是container.__contains__(x):
# x in container 等价于 container.__contains__(x)
# 如果对象没有__contains__方法,Python会退而求其次:
# 通过迭代查找,即逐个比较 __iter__() 的元素
# 等价于:
# for item in container:
# if item == x:
# return True
# return False
# 演示:自定义类的in操作
class Classroom:
def __init__(self, students):
self.students = students
def __contains__(self, student_name):
"""自定义in操作:支持模糊匹配"""
return any(student_name in s for s in self.students)
classroom = Classroom(["张三", "张三丰", "李四", "王五"])
print("张三" in classroom) # True —— 完全匹配
print("张" in classroom) # True —— 模糊匹配!因为"张" in "张三"
print("李四" in classroom) # True
print("赵六" in classroom) # False
2.3 in 在不同容器中的查找效率
这是实际开发中非常重要的知识点:
import timeit
# 准备测试数据
n = 100000
test_list = list(range(n))
test_set = set(range(n))
test_dict = {i: i for i in range(n)}
test_tuple = tuple(range(n))
target = 99999
# 列表 —— O(n) 线性查找
t_list = timeit.timeit(lambda: target in test_list, number=1000)
print(f"列表查找: {t_list:.4f}s")
# 集合 —— O(1) 哈希查找
t_set = timeit.timeit(lambda: target in test_set, number=1000)
print(f"集合查找: {t_set:.4f}s")
# 字典 —— O(1) 哈希查找
t_dict = timeit.timeit(lambda: target in test_dict, number=1000)
print(f"字典查找: {t_dict:.4f}s")
# 元组 —— O(n) 线性查找(和列表一样)
t_tuple = timeit.timeit(lambda: target in test_tuple, number=1000)
print(f"元组查找: {t_tuple:.4f}s")
# 📝 结论:
# - 如果你需要频繁做in检查,用set或dict(O(1))
# - list/tuple的in是O(n),数据量大时很慢
# - str的in用于子串查找,算法复杂度O(n*m)
2.4 in 的高级用法
# 1. 多值检查
# 检查是否存在多个值中的任意一个
def contains_any(text, words):
"""检查text中是否包含words中的任意一个词"""
return any(word in text for word in words)
text = "Python是人工智能领域最流行的编程语言"
print(contains_any(text, ["Java", "Python", "C++"])) # True
print(contains_any(text, ["Java", "Go", "Rust"])) # False
# 2. 结合for循环
# 遍历的同时检查
valid_items = ["apple", "banana", "orange"]
user_items = ["apple", "grape", "banana", "kiwi"]
# 筛选有效的物品
valid_user_items = [item for item in user_items if item in valid_items]
print(valid_user_items) # ['apple', 'banana']
# 3. 检查字符串是否包含某些模式
import re
# 简单模式用in,复杂模式用正则
text = "联系邮箱: zhangsan@example.com, 电话: 13800138000"
# in 用于简单匹配
has_email = "@" in text and ".com" in text
print(has_email) # True
# 4. 使用集合进行高效多值检查
# 检查用户输入是否在允许值之中
ALLOWED_OPERATIONS = {"add", "delete", "update", "query"}
user_op = "delete"
if user_op in ALLOWED_OPERATIONS: # O(1)!
print(f"执行{user_op}操作")
# 5. 字典键的批量检查
config = {"host": "localhost", "port": 8080, "debug": True}
required_keys = {"host", "port"}
# 检查所有必需的键是否都存在
if required_keys <= config.keys(): # 子集检查
print("所有必需的配置项都已设置")
# 或者
if all(key in config for key in required_keys):
print("所有必需的配置项都已设置")
2.5 in 与 for 中的 in 的区别
# for 中的 in 不是运算符,而是语法的一部分
# for item in iterable:
# pass
# 成员运算符 in 用于判断
# item in container → 返回 True/False
# 它们的区别:
fruits = ["苹果", "香蕉"]
# for中的in —— 语法关键字,用于迭代
for fruit in fruits:
print(fruit)
# 表达式中的in —— 成员运算符,返回布尔值
print("苹果" in fruits) # True
# 两者可以一起使用
text = "Python is great"
vowels = {'a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U'}
# for循环遍历字符,in检查是否为元音
vowel_chars = [c for c in text if c in vowels]
print(vowel_chars) # ['o', 'i', 'e', 'a']
print(f"元音数量: {len(vowel_chars)}")
三、身份运算符 is 与 is not
3.1 基本概念:is 检查的是身份,不是值
# is:判断两个变量是否指向同一个对象(比较id)
# is not:判断两个变量是否指向不同的对象
# is vs == 的核心区别
a = [1, 2, 3]
b = [1, 2, 3]
c = a
print(a == b) # True —— 值相等
print(a is b) # False —— 但不是同一个对象
print(a is c) # True —— c就是a
# 图解:
# a ──→ [1, 2, 3] ←── c
# b ──→ [1, 2, 3] (另一个独立的列表)
3.2 is 的本质:比较 id()
# is 等价于比较两个对象的 id()
# a is b ≈ id(a) == id(b)
x = [1, 2, 3]
y = [1, 2, 3]
z = x
print(id(x))
print(id(y))
print(id(z))
print(x is y) # False —— id(x) != id(y)
print(x is z) # True —— id(x) == id(z)
print(id(x) == id(y)) # False —— 等同于 x is y
3.3 is 用于与单例对象比较
这是is最重要、最推荐的用法:
# ✅ 推荐:使用 is 比较 None
def process_data(data=None):
if data is None:
data = []
# ...
# ❌ 不推荐:使用 == 比较 None
def process_data_bad(data=None):
if data == None: # 可以用,但不推荐
data = []
# 为什么推荐 is?
# 1. is 不能被自定义的 __eq__ 覆盖
# 2. is 更快(直接比较id)
# 3. is 更符合语义(None是单例)
# 可以用 is 比较的单例对象:
# None, True, False
x = True
print(x is True) # True —— 推荐
# 以及自定义的单例
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
s1 = Singleton()
s2 = Singleton()
print(s1 is s2) # True —— 单例模式
3.4 小整数缓存:is 的著名陷阱
这是Python面试中的高频题目:
# Python缓存了 -5 到 256 之间的小整数
a = 256
b = 256
print(a is b) # True —— 同一个缓存对象
a = 257
b = 257
print(a is b) # False —— 超出了缓存范围!
# 为什么有缓存?
# 小整数使用频繁,缓存可以避免重复创建对象,节省内存
# 查看小整数的id
for i in [255, 256, 257]:
a = i
b = i
print(f"{i:>4}: a is b = {a is b}")
# ⚠️ 注意:不要依赖这个行为!
# 这是CPython的实现细节,其他Python实现可能不同
# 而且缓存范围可能随版本变化
# 字符串也有驻留(interning)机制,但规则更复杂
a = "hello"
b = "hello"
print(a is b) # True —— 短字符串可能被驻留
a = "hello world!"
b = "hello world!"
print(a is b) # True —— 看起来像标识符的字符串会被驻留
a = "hello world!@#"
b = "hello world!@#"
print(a is b) # False —— 包含特殊字符可能不被驻留
# 📝 铁律:
# 比较值 → 用 ==
# 比较None → 用 is
# 其他情况 → 不要依赖 is 的行为!
3.5 is 不可被覆盖
与==不同,is运算符的行为是固定的,不能被__eq__等魔术方法覆盖:
class WeirdClass:
"""一个奇怪的类:__eq__永远返回True"""
def __eq__(self, other):
return True
w = WeirdClass()
# == 被覆盖了
print(w == None) # True —— 因为__eq__返回True
print(w == 42) # True
print(w == "hello") # True
# is 不受影响!
print(w is None) # False —— w确实不是None
print(w is w) # True
# 这就是为什么比较None时要用 is
# 如果有人恶意或不小心覆盖了__eq__,== None 就会出错
四、综合对比与应用场景
4.1 in vs == 的对比
# == 检查"等于"(单个值比较)
# in 检查"属于"(容器成员检查)
# 场景1:检查一个值是否是多个允许值之一
# 差:用多个 == 和 or
status = "active"
if status == "active" or status == "pending" or status == "processing":
print("有效状态")
# 好:用 in
if status in ("active", "pending", "processing"):
print("有效状态")
# 更好:用集合(更高效,特别是选项多时)
VALID_STATUSES = frozenset(["active", "pending", "processing"])
if status in VALID_STATUSES:
print("有效状态")
# 场景2:字符串匹配
name = "Alice"
# 精确匹配用 ==
print(name == "Alice") # True
# 部分匹配用 in
print("Ali" in name) # True
print("lice" in name) # True
# 前缀匹配用 startswith / endswith
print(name.startswith("Al")) # True
print(name.endswith("ce")) # True
4.2 is vs == 的选择指南
# === 决策树 ===
# 1. 比较的是 None 吗?
# 是 → 用 is / is not
# 否 → 继续
# 2. 比较的是 True / False 吗?
# 是 → 用 is / is not(或者直接用真值测试)
# 否 → 继续
# 3. 比较的是单例对象吗?(如自定义的单例、枚举等)
# 是 → 用 is / is not
# 否 → 继续
# 4. 需要检查两个变量是否指向同一对象吗?
# 是 → 用 is / is not
# 否 → 用 == / !=
# === 实例代码 ===
# ✅ 正确用法
if result is None:
print("没有结果")
if flag is True: # 或者直接 if flag:
print("标志为真")
if a is b: # 就想知道是不是同一对象
print("同一对象")
# ✅ 正确用法(值比较)
if name == "张三":
print("找到张三")
if a == b: # 想知道值是否相等
print("值相等")
# ❌ 错误用法
if name is "张三": # 用is比较字符串值!(可能碰巧对,但不可靠)
print("找到张三")
if result == None: # 用==比较None
print("没有结果")
五、性能对比与选择策略
5.1 in 在不同容器中的性能
import timeit
# 小数据量(10个元素)
small_list = list(range(10))
small_set = set(range(10))
small_tuple = tuple(range(10))
target = 5
print("=== 小数据量(10个元素) ===")
print(f"列表: {timeit.timeit(lambda: target in small_list, number=10_000_000):.3f}s")
print(f"集合: {timeit.timeit(lambda: target in small_set, number=10_000_000):.3f}s")
print(f"元组: {timeit.timeit(lambda: target in small_tuple, number=10_000_000):.3f}s")
# 大数据量(100000个元素)
large_list = list(range(100000))
large_set = set(range(100000))
target = 99999
print("\n=== 大数据量(100000个元素) ===")
print(f"列表: {timeit.timeit(lambda: target in large_list, number=1000):.4f}s")
print(f"集合: {timeit.timeit(lambda: target in large_set, number=1000):.4f}s")
# 📝 结论:
# - 小数据量:差异不大,列表甚至可能更快(没有哈希开销)
# - 大数据量:集合/字典碾压列表/元组
# - 分界线大约在几十到几百个元素
# - 如果需要频繁查找,总是优先考虑set/dict
5.2 is 的性能
import timeit
a = [1, 2, 3]
b = [1, 2, 3]
c = a
# is 非常快 —— 只是比较两个整数(id)
t_is_same = timeit.timeit(lambda: a is c, number=10_000_000)
t_is_diff = timeit.timeit(lambda: a is b, number=10_000_000)
# == 相对慢 —— 对于列表需要逐元素比较
t_eq = timeit.timeit(lambda: a == b, number=10_000_000)
print(f"is (同一对象): {t_is_same:.3f}s")
print(f"is (不同对象): {t_is_diff:.3f}s")
print(f"== (不同对象但值同): {t_eq:.3f}s")
print(f"is比==快: {t_eq / t_is_diff:.0f}x")
六、实战案例
6.1 URL路由匹配器
class SimpleRouter:
"""简单的URL路由匹配器,展示 in 的实际应用"""
def __init__(self):
self.routes = {}
def add_route(self, path, handler):
self.routes[path] = handler
def dispatch(self, path):
"""路由分发"""
# 精确匹配 O(1)
if path in self.routes:
return self.routes[path]()
# 前缀匹配 O(n) —— 检查是否有前缀路由
for route_path, handler in self.routes.items():
if path.startswith(route_path):
return handler()
return self.not_found()
def not_found(self):
return "404 Not Found"
# 使用
router = SimpleRouter()
router.add_route("/", lambda: "首页")
router.add_route("/about", lambda: "关于页")
router.add_route("/api/users", lambda: "用户API")
router.add_route("/api/", lambda: "API入口")
print(router.dispatch("/")) # 首页
print(router.dispatch("/about")) # 关于页
print(router.dispatch("/api/users")) # 用户API
print(router.dispatch("/api/products")) # API入口(前缀匹配)
print(router.dispatch("/contact")) # 404 Not Found
6.2 权限检查装饰器
from functools import wraps
class PermissionChecker:
"""基于 in 运算符的权限检查器"""
def __init__(self):
self.user_permissions = {}
def grant(self, user, permission):
if user not in self.user_permissions:
self.user_permissions[user] = set()
self.user_permissions[user].add(permission)
def revoke(self, user, permission):
if user in self.user_permissions:
self.user_permissions[user].discard(permission)
def has_permission(self, user, permission):
"""检查权限 —— O(1)"""
return user in self.user_permissions and \
permission in self.user_permissions[user]
def require(self, permission):
"""权限检查装饰器"""
def decorator(func):
@wraps(func)
def wrapper(user, *args, **kwargs):
if self.has_permission(user, permission):
return func(user, *args, **kwargs)
else:
raise PermissionError(
f"用户 {user} 没有 {permission} 权限"
)
return wrapper
return decorator
# 使用
checker = PermissionChecker()
checker.grant("admin", "delete_user")
checker.grant("admin", "create_user")
checker.grant("editor", "edit_article")
@checker.require("delete_user")
def delete_user(user, target_user):
print(f"{user} 删除了用户 {target_user}")
@checker.require("edit_article")
def edit_article(user, article_id):
print(f"{user} 编辑了文章 {article_id}")
# 测试
try:
delete_user("admin", "user123")
delete_user("editor", "user456") # editor没有delete_user权限
except PermissionError as e:
print(f"❌ {e}")
try:
edit_article("editor", "art001")
except PermissionError as e:
print(f"❌ {e}")
6.3 缓存管理器(结合 is 和 in)
class CacheManager:
"""缓存管理器,展示 is 和 in 的组合使用"""
SENTINEL = object() # 使用唯一对象作为"不存在"的标记
def __init__(self):
self._cache = {}
def get(self, key, default=None):
"""从缓存获取值"""
value = self._cache.get(key, self.SENTINEL)
# ✅ 用 is 比较哨兵对象
if value is self.SENTINEL:
if default is not None:
return default
return None
return value
def set(self, key, value):
"""设置缓存"""
# ✅ 用 in 检查键是否存在
if key in self._cache:
print(f"⚠️ 键 {key!r} 已存在,将被覆盖")
self._cache[key] = value
return value
def has(self, key):
"""检查键是否存在"""
return key in self._cache
def get_or_set(self, key, factory):
"""获取缓存值,如果不存在则调用factory创建并缓存"""
if key not in self._cache:
self._cache[key] = factory()
return self._cache[key]
# 使用
cache = CacheManager()
# 模拟耗时的数据库查询
def query_database():
import time
time.sleep(0.1) # 模拟耗时
return {"name": "张三", "age": 25}
# 第一次调用会执行查询
user1 = cache.get_or_set("user:1", query_database)
print(f"第一次: {user1}")
# 第二次直接命中缓存,不执行查询
user2 = cache.get_or_set("user:1", query_database)
print(f"第二次: {user2}")
# 验证是同一个对象(缓存生效)
print(f"是同一对象: {user1 is user2}") # True
七、常见陷阱与最佳实践
7.1 陷阱汇总
# 陷阱1:用 is 比较数字
a = 1000
b = 1000
print(a is b) # False —— 超出了小整数缓存范围
print(a == b) # True —— 这才是正确的值比较
# 陷阱2:用 in 检查字典的值
d = {"name": "张三", "age": 25}
print("张三" in d) # False —— in检查的是键!
print("张三" in d.values()) # True —— 检查值需要.values()
# 陷阱3:用 in 在长列表中频繁查找
# ❌ 差:O(n) 每次
large_list = list(range(100000))
for i in range(1000):
if i in large_list:
pass
# ✅ 好:先转成集合,O(1) 每次
large_set = set(large_list)
for i in range(1000):
if i in large_set:
pass
# 陷阱4:可变对象作为字典键
# ❌ 错误
# d = {[1, 2]: "value"} # TypeError: unhashable type: 'list'
# 列表不可哈希,不能作为字典键或集合元素
# ✅ 正确
d = {(1, 2): "value"} # 元组可哈希
# 这样 in 检查才有效
print((1, 2) in d) # True
# 陷阱5:is 与 == 混用
class AlwaysEqual:
def __eq__(self, other):
return True
obj = AlwaysEqual()
print(obj == None) # True(__eq__被覆盖了)
print(obj is None) # False(is不受影响,这才是对的)
7.2 最佳实践总结
# ===== in 的最佳实践 =====
# 1. 频繁查找用 set/dict,不用 list/tuple
VALID = frozenset(["a", "b", "c"]) # 不可变集合,适合常量
# 2. 检查字典键存在用 in(或 get 方法)
if "key" in my_dict:
value = my_dict["key"]
# 3. 检查子串用 in
if "error" in log_message.lower():
handle_error()
# 4. 多选一条件用 in
if status in ("active", "pending", "processing"):
pass
# ===== is 的最佳实践 =====
# 1. 比较 None 永远用 is
if value is None: # ✅
pass
# 2. 比较 True/False 可以用 is(但通常直接用真值测试)
if flag: # ✅ 最Pythonic
pass
# 3. 比较值永远用 ==
if name == "张三": # ✅
pass
# 4. 需要检查"是同一个对象"时用 is
if a is b: # ✅ 明确意图
pass
八、本章小结
✅ 本文我们深入学习了Python的两类特殊运算符:
成员运算符 in 与 not in:
- 检查元素是否在可迭代对象中(调用
__contains__) - 字典的
in检查的是键,不是值 - 字符串的
in可以检查子串 - 集合/字典的
in是O(1),列表/元组的in是O(n) - 自定义类可以通过
__contains__定制in的行为
身份运算符 is 与 is not:
- 比较的是对象的内存地址(id),不是值
is的行为不可被覆盖(而==可以)- 比较None永远用
is(这是最重要的实践) - 小整数缓存(-5~256)会让
is的行为出乎意料 - 不要用
is比较值——那属于==的职责
理解in和is的底层机制,不仅能帮你写出正确的代码,还能帮你做出更好的性能决策。⌨️ 下一篇文章,我们将学习运算符优先级对照表与记忆方法,把所有运算符的知识串联起来!
803

被折叠的 条评论
为什么被折叠?



