在Python日常编码中,in/not in和is/is not几乎随处可见,但容器类型、魔术方法、CPython缓存都会影响结果。理解它们的语义、底层调用和性能差异,才能避免把“碰巧正确”当成稳定行为。
一、in/not in的语义与容器差异
in用于判断成员是否属于容器,not in取反。字符串判断子串且大小写敏感;列表、元组按元素值查找;字典检查的是键而不是值,若要查值需使用values();集合按哈希成员判断。
- print('Py' in 'Python')
- print('py' in 'Python')
- print('X' not in 'Python')
- fruits = ['苹果', '香蕉', '橘子']
- print('苹果' in fruits)
- print('西瓜' not in fruits)
- user = {'name': '张三', 'age': 25, 'city': '北京'}
- print('name' in user)
- print('张三' in user)
- print('张三' in user.values())
- valid_colors = {'red', 'green', 'blue'}
- print('red' in valid_colors)
- print('yellow' in valid_colors)
复制代码
二、底层机制:__contains__与迭代回退
表达式x in container优先调用container.__contains__(x)。如果对象没有实现该方法,Python会退化为迭代查找,逐个用==比较元素。自定义类可以实现__contains__,从而控制in的判定逻辑。
- class Classroom:
- def __init__(self, students):
- self.students = students
- def __contains__(self, student_name):
- return any(student_name in s for s in self.students)
- classroom = Classroom(['张三', '张三丰', '李四', '王五'])
- print('张三' in classroom)
- print('张' in classroom)
- print('李四' in classroom)
- print('赵六' in classroom)
复制代码
这里__contains__内部又使用了字符串in,因此“张”能匹配“张三”,形成模糊匹配效果。这说明in的行为不仅取决于运算符本身,还取决于容器或自定义对象提供的成员判断实现。
三、查找效率与容器选择
列表和元组的in查找通常是O(n)线性扫描;集合和字典基于哈希,平均为O(1);字符串的子串查找复杂度为O(n*m)。数据量大且需要频繁判断成员时,应优先考虑set或dict。
- 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
- t_list = timeit.timeit(lambda: target in test_list, number=1000)
- t_set = timeit.timeit(lambda: target in test_set, number=1000)
- t_dict = timeit.timeit(lambda: target in test_dict, number=1000)
- t_tuple = timeit.timeit(lambda: target in test_tuple, number=1000)
- print(f'列表: {t_list:.4f}s')
- print(f'集合: {t_set:.4f}s')
- print(f'字典: {t_dict:.4f}s')
- print(f'元组: {t_tuple:.4f}s')
复制代码
性能结论要结合数据规模看:小数据量时差异可能不大,列表甚至可能因没有哈希开销而不慢;大数据量下集合和字典优势明显。分界线通常在几十到几百个元素附近,频繁查找场景应直接使用set/dict。
四、for中的in与成员运算符
for item in iterable中的in是语法关键字,用于迭代;表达式item in container中的in是成员运算符,返回布尔值。两者可以同时出现在列表推导式中。
- text = 'Python is great'
- vowels = {'a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U'}
- vowel_chars = [c for c in text if c in vowels]
- print(vowel_chars)
- print(f'元音数量: {len(vowel_chars)}')
复制代码
五、is/is not:对象身份而非值
is判断两个变量是否指向同一个对象,本质是比较id();==判断值是否相等。两个内容相同的列表可能==为True,但is为False;让一个变量直接指向另一个变量后,is才为True。
- a = [1, 2, 3]
- b = [1, 2, 3]
- c = a
- print(a == b)
- print(a is b)
- print(a is c)
- print(id(a) == id(b))
复制代码
is最适合与None、True、False等单例对象比较。比较None时推荐用is None,因为is不受自定义__eq__影响,速度也更快,语义上更符合“是否为同一个单例对象”。
小整数缓存是is的著名陷阱。CPython会缓存-5到256之间的小整数,因此两个变量分别赋值为256时可能is为True,赋值为257时可能is为False。但这是实现细节,不应在业务代码中依赖。
- a = 256
- b = 256
- print(a is b)
- a = 257
- b = 257
- print(a is b)
复制代码
字符串也有驻留机制,但规则更复杂。看起来像标识符的短字符串可能被驻留,包含特殊字符的字符串则不一定。下面的结果只应作为观察,不能作为稳定判断依据。
- a = 'hello'
- b = 'hello'
- print(a is b)
- a = 'hello world!'
- b = 'hello world!'
- print(a is b)
- a = 'hello world!@#'
- b = 'hello world!@#'
- print(a is b)
复制代码
is的行为固定,不能被__eq__覆盖。下面的类让==几乎永远返回True,但is仍然只认对象身份。
- class WeirdClass:
- def __eq__(self, other):
- return True
- w = WeirdClass()
- print(w == None)
- print(w == 42)
- print(w is None)
- print(w is w)
复制代码
因此,比较值用==,比较None用is,其他情况不要依赖is的缓存或驻留行为。
六、in与==、is与==的选择
检查一个值是否属于多个允许值之一时,用in比串联多个==更清晰;选项较多时可用set或frozenset提升查找效率。字符串精确匹配用==,子串匹配用in,前后缀匹配用startswith/endswith。
- status = 'active'
- VALID_STATUSES = frozenset(['active', 'pending', 'processing'])
- if status in VALID_STATUSES:
- print('有效状态')
- name = 'Alice'
- print(name == 'Alice')
- print('Ali' in name)
- print(name.startswith('Al'))
- print(name.endswith('ce'))
复制代码
选择策略可以归纳为:比较None、True、False或自定义单例用is;比较普通值用==;判断成员属于容器用in;不要用is比较字符串值,也不推荐用==比较None。
七、实战:路由匹配与权限检查
URL路由中,精确路径可以用字典键做O(1)匹配,前缀路由则遍历检查startswith。这个例子能同时体现in、字典查找和前缀匹配的取舍。
- class SimpleRouter:
- def __init__(self):
- self.routes = {}
- def add_route(self, path, handler):
- self.routes[path] = handler
- def dispatch(self, path):
- if path in self.routes:
- return self.routes[path]()
- 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'))
- print(router.dispatch('/api/products'))
- print(router.dispatch('/contact'))
复制代码
权限检查中,用集合保存用户权限,再用in判断权限是否存在,平均复杂度为O(1)。结合装饰器可以把权限校验从业务函数中抽离出来。
- from functools import wraps
- class PermissionChecker:
- 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):
- 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)
- raise PermissionError(f'用户 {user} 没有 {permission} 权限')
- return wrapper
- return decorator
- checker = PermissionChecker()
- checker.grant('admin', 'delete_user')
- checker.grant('editor', 'edit_article')
- @checker.require('delete_user')
- def delete_user(user, target_user):
- print(f'{user} 删除了用户 {target_user}')
- try:
- delete_user('admin', 'user123')
- delete_user('editor', 'user456')
- except PermissionError as e:
- print(f'❌ {e}')
复制代码
八、常见陷阱与最佳实践
常见陷阱包括:把字典的in误认为检查值;用is比较字符串值;用==比较None;在大型列表上频繁做in判断;依赖小整数缓存或字符串驻留结果。最佳实践是:值比较用==,None和单例判断用is,成员判断用in;频繁成员检查改用set或dict;字符串子串用in,精确值用==,前后缀用startswith/endswith;自定义__eq__存在时更要用is判断None。正确选择运算符和容器,既影响代码正确性,也直接影响查找性能。 |