尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Python身份运算符is与==区别:从内存地址到对象驻留机制详解

Python身份运算符is与==区别:从内存地址到对象驻留机制详解 身份运算符这个词学 Python 的朋友应该都不陌生但熟悉不一定代表真懂。我见过不少写了两三年 Python 的人一到面试被问“is 和 有什么区别”就开始支支吾吾。原因也很简单日常业务代码里大家用 的频率远高于用 is而 is 往往只在判断 None 的时候出现用得少自然就没什么深刻印象。这章就把身份运算符彻底讲透。它只有两个成员is和is not作用是判断两个对象“是不是同一个对象”。注意这里说的是“同一个对象”不是“内容相等”。这两个概念之间的差别是整个章节的灵魂也是无数人掉坑的地方。我会从身份的本质讲起把id()、小整数缓存、字符串驻留这些底层机制全部串起来再配上大量可复现的代码实验希望你看完之后不仅能应付面试还能在实际项目里少踩几个隐蔽的坑。1. 身份运算符到底在判断什么先搞清楚“身份”的定义1.1 生活中的身份和 Python 里的身份一说“身份”这个词你肯定能联想到身份证。在生活里判断两个人是不是同一个人不能只看名字相同因为同名同姓的人太多了得看身份证号。Python 里的“身份”也是这个道理。每一个对象在创建的时候解释器都会给它分配一个独一无二的标识这个标识在对象的整个生命周期里不会改变我们可以把它理解成对象的“身份证号”。在 CPython 实现里这个“身份证号”实际上就是对象在内存中的地址。你可以用内置函数id()把它取出来看a [1, 2, 3] b [1, 2, 3] print(id(a)) print(id(b))你会发现两次打印出来的数字不一样。这说明a和b虽然内容一样但它们是两个不同的对象住在内存里两个不同的“房间”。而身份运算符is判断的就是这两个变量到底指向不指向同一个“房间”。1.2 用 is 比较的是“同一个对象”不是“值相等”这是整章最核心的一句话is比较的是对象身份也就是id()的返回值是否相同比较的是对象的值是否相等。光说概念有点干直接看代码a [1, 2, 3] b [1, 2, 3] c a print(a b) # True值相等 print(a is b) # False不是同一个对象 print(a is c) # Truec 就是 a 本身这里a和b是分别创建的两个列表虽然里面的元素一模一样但它们是两个独立的对象。c a这个赋值操作并没有复制一个新的列表而是让c和a指向同一个列表对象所以a is c为True。理解这一层之后很多初学者会问那既然能判断内容相等is还有什么用别急往下看。1.3 身份运算符的标准语法和返回值身份运算符的语法非常简单只有两种形式a is b如果a和b指向同一个对象返回True否则返回False。a is not b如果a和b指向不同的对象返回True否则返回False。注意is not是一个整体不是先is再取反所以不能写成not a is b这种别扭的样式。官方推荐的做法始终是a is not b可读性最强。另外is的返回值一定是布尔值True或False不会像普通比较运算符那样在特殊场景下返回其他对象。这一点在条件判断里特别好用因为你可以直接写if result is not None: # 处理结果 pass干净、明确、不容易出错。2. is 和 到底差在哪值相等与身份相同是两码事2.1 比较的是内容is 比较的是容器本身用一个生活化的类比假设你有一个一模一样的手机我和你的手机外观、型号、序列号完全一致用来比较它是True因为“内容”相同。但这两台物理上的手机显然不是一个东西用is来比较它是False。在 Python 里实际上是在调用对象的__eq__方法这个方法可以被每个类自由重写。比如字符串比较时忽略大小写、自定义类比较时只比较某个字段这些都可以由开发者自己定义。但is没有这种灵活性它就是硬生生比较id()没有任何“商量”的余地。举一个实际项目里的例子。你在写接口对接时收到两个 JSON 解析出来的列表它们的值一样但一个是请求体解析来的一个是缓存里读出来的data_from_request [1, 2, 3] data_from_cache [1, 2, 3] print(data_from_request data_from_cache) # True print(data_from_request is data_from_cache) # False这里告诉你“内容一致”可以放心用is告诉你“这两个对象不是同一个”如果代码里有人意外改了其中一个另一个不会跟着变。所以在判断“业务上是否相等”时用在判断“是不是同一个实例”时用is各有各的用途。2.2 从底层机制看两者的差异如果你好奇为什么能“自定义”那是因为当你写a b时Python 实际执行的是a.__eq__(b)。对于内置类型Python 已经定义好了比较规则对于自定义类你也可以自己实现__eq__。而a is b这种写法Python 不会去调用任何特殊方法它直接比较两个对象的 id。看一下这个自定义类class Person: def __init__(self, name): self.name name def __eq__(self, other): if not isinstance(other, Person): return NotImplemented return self.name other.name p1 Person(张三) p2 Person(张三) print(p1 p2) # True因为我重写了 __eq__只比较名字 print(p1 is p2) # False它们是两个不同的 Person 实例这段代码很有意思业务上我觉得两个名字相同的人“相等”所以重写了__eq__但从内存角度看p1和p2是两个独立实例身份并不相同。很多框架源码里判断两个对象是否“同一个”时都会优先用is因为它是绝对可靠的“身份认证”不受任何重写逻辑影响。2.3 一张表看懂两者差异为了让你一眼抓住重点我把最关键的差异整理成一张表比较方式判断内容底层机制是否可自定义典型用途值是否相等调用__eq__方法可以重写业务判断、内容比较is是否为同一个对象比较id()不可重写身份判断、None 判断这张表建议你存下来。面试里被问到“is 和 的区别”把这几点说清楚就基本满分了。2.4 为什么初学者总在这里翻车翻车的根源多半是把“值相等”和“同一个对象”混为一谈。初学者看到两个变量打印出来一样就下意识认为它们“是”一个东西。尤其是在操作列表、字典这种可变对象时用判断相等后又去修改其中一个变量结果发现另一个也变了这才反应过来它俩其实是同一个对象但为时已晚。我给个最常见的场景original [1, 2, 3] backup original backup.append(4) print(original) # [1, 2, 3, 4]backup original并没有复制列表只是让两个名字都指向同一个列表对象。如果想让backup成为真正的副本得用original.copy()或者original[:]。这个坑就是典型的不理解“身份”导致的。所以我说搞懂is不仅能回答面试题还能避免一堆隐蔽的 bug。3. 身份运算符背后的机制小整数缓存、字符串驻留与对象生命周期3.1 id() 函数查看对象的身份证号前面提到id()返回的是对象的唯一标识。在 CPython 中这个标识就是内存地址所以你可以通过两次id()是否相同判断两个对象是不是同一个。不过直接打印内存地址有一个小陷阱如果对象已经被垃圾回收了这个地址可能被新的对象复用。所以在做id()实验时要保证对象还活着。比如这样a [1, 2, 3] print(id(a))只要a还在作用域里这个地址就是有效的。你可以把它理解成一个门牌号只要这个“房间”里的人还没搬走门牌号就不会变。3.2 小整数缓存-5 到 256 之间的整数为什么 is 结果为 True这是身份运算符最经典的考点几乎每次聊到is都会提到。CPython 在启动的时候会预先创建一批整数对象范围是 -5 到 256。也就是说这个范围内的整数不管你在代码里写多少次拿到的都是同一个预先创建好的对象。于是会出现这种看起来“违反常识”的现象a 256 b 256 print(a is b) # True c 257 d 257 print(c is d) # False256因为落在缓存范围内a和b都指向同一个整数对象所以is为True。257超出了缓存范围解释器会为c和d分别创建新的整数对象所以is为False。这里要特别提醒不要在实际业务里依赖这个行为。Python 官方从来没有承诺过小整数缓存一定存在不同的 Python 实现比如 PyPy、Jython可能有不同的规则。把is用在整数比较上是典型的“在刀刃上跳舞”看着爽翻车也快。3.3 字符串驻留机制什么时候字符串会被复用整数有小整数缓存字符串也有类似的机制叫“字符串驻留”。简单说某些字符串在创建时会被放进一个内部表里后面再创建相同内容的字符串时如果发现表里已经有了就直接返回同一个对象。什么样的字符串会被驻留在 CPython 里规则比较复杂但有一个常见规律只包含字母、数字、下划线的短字符串在代码中直接以字面量形式出现时通常会被驻留。比如s1 hello s2 hello print(s1 is s2) # True短字符串且全是合法标识符字符 t1 hello world t2 hello world print(t1 is t2) # False包含空格通常不驻留注意这里的规则在不同的 Python 版本里可能有细微差别。我在本地用 3.11 测试hello和hello是同一个对象但在某些版本或者某些交互环境下结果可能不一样。所以和整数一样不要依赖字符串的驻留行为老老实实用判断字符串内容才是王道。3.4 可变对象和不可变对象对 is 的影响在 Python 里列表、字典、集合是可变对象整数、字符串、元组是不可变对象。身份运算符对这两类对象的表现也有一定的规律可循。对不可变对象来说如果值相同Python 有时会复用对象比如小整数缓存和字符串驻留所以is偶尔会返回True。对可变对象来说每次通过字面量创建基本都是全新的对象两个内容相同的列表用is比较结果几乎总是Falselist1 [1, 2, 3] list2 [1, 2, 3] print(list1 is list2) # False tuple1 (1, 2, 3) tuple2 (1, 2, 3) print(tuple1 is tuple2) # 这个结果不确定可能与编译优化有关因为元组是不可变对象某些情况下解释器会做常量折叠把代码中相同的元组字面量合并成一个对象。但这仍然不是可靠的行为实验一下可以别拿它当业务逻辑。4. 身份运算符的正确使用场景与实战演示4.1 判断 NonePEP 8 强烈推荐 is None身份运算符在实际项目里最常用的场景就是和None比较。None是 Python 里的一个单例对象整个程序运行期间只有一个None实例。所以判断一个变量是不是None正确的写法是if result is None: # 处理空值 pass if result is not None: # 正常流程 pass为什么不用 None一方面PEP 8 明确建议用is判断单例包括None另一方面某些对象重写了__eq__可能导致x None返回不是你想要的结果。比如numpy的数组arr None会返回一个数组而不是布尔值这在条件判断里会引发异常。is None则永远返回确定的布尔值绝对安全。4.2 单例模式与全局唯一对象判断如果你写过单例模式对is一定不陌生。单例模式保证一个类只有一个实例那么判断一个对象是不是这个唯一的实例用is是最直接的方式。来看一个简单的单例实现class Config: _instance None def __new__(cls): if cls._instance is None: cls._instance super().__new__(cls) return cls._instance cfg1 Config() cfg2 Config() print(cfg1 is cfg2) # True因为始终返回同一个实例这里__new__里判断_instance is None就是在检查单例实例是否已经创建。这个场景下如果用反而画蛇添足因为你关心的是“是不是同一个实例”而不是“两个实例的内容相不相等”。4.3 缓存池与对象复用场景很多框架内部会做对象池、缓存池这时候也需要用is判断对象是否来自池子。比如数据库连接池从池里拿出来的连接对象和归还的连接对象如果没有被销毁重建就应该是同一个对象。用is可以快速判断连接是否“存活于池子中”。还有一种常见场景是functools.lru_cache这类缓存装饰器。它缓存的是返回值如果你多次调用同一个参数的函数拿到的返回值可能是同一个对象。这时候用is可以验证缓存是否真正命中from functools import lru_cache lru_cache(maxsizeNone) def get_data(key): return [key, key * 2] r1 get_data(10) r2 get_data(10) print(r1 is r2) # True因为第二次调用直接命中了缓存这个特性在性能优化时很有用当你希望多次计算结果复用同一个对象来节省内存时可以用is来验证你的缓存设计是否真的生效。4.4 用 is 排查对象是否被意外复制在实际调试中is还能帮你确认一个对象是否被“意外复制”了。比如你写了一个函数希望它直接修改传入的列表但调用后发现原来的列表没变怀疑函数内部是不是复制了一份。这时候用is一测就知道def process(data): # 如果这里写了 data data[:]就会产生新的列表 data data[:] data.append(99) return data original [1, 2] result process(original) print(original is result) # False说明 process 内部复制了类似地如果你实现的函数希望返回传入的对象本身而不是副本也可以在外层用is做防御性断言。这种调试方式比肉眼读代码判断快得多。5. 常见问题与排查技巧实录5.1 为什么 256 is 256 是 True257 is 257 却是 False这是初学者问得最多的问题答案就是前面说的小整数缓存。在 CPython 里-5 到 256 的整数在启动时就预创建好了所以代码里出现的所有 256 都指向同一个对象超出这个范围的整数每次都可能新建对象。遇到这个问题的排查方法很简单用id()打印一下两个变量的地址一看便知。c 257 d 257 print(id(c)) print(id(d))如果两个地址不一样那c is d自然是False。这里的教训是永远不要拿is去比较整数用才是正道。5.2 为什么两个内容相同的字符串is 有时返回 True有时返回 False字符串的驻留机制导致了这个“薛定谔的 is”。在交互式环境或者不同版本的 Python 中行为可能不一致。比如a hello b hello print(a is b) # 通常 True因为 hello 会被驻留 c hello world d hello world print(c is d) # 结果不确定取决于是否触发编译期优化排查这类问题建议直接把字符串运算结果用id()打印出来对比地址。同时记住一条铁律判断字符串内容是否相等一律用。你可能会觉得既然有驻留机制用is不是更快吗实际上大多数情况下这点性能差异可以忽略但用错带来的 bug 风险却很大完全得不偿失。5.3 is 和 在自定义类对象上的差异自定义类如果没有重写__eq__那么和is的结果是一样的因为默认的__eq__就是比较身份class Point: def __init__(self, x, y): self.x x self.y y p1 Point(1, 2) p2 Point(1, 2) print(p1 p2) # False因为没有重写 __eq__ print(p1 is p2) # False但一旦重写了__eq__和is就可能出现分歧这正好可以用来看清楚两者的职责。在这里is是可靠的“同一个对象”判断是业务上的“值相等”判断。写自定义类时建议同时实现__eq__和__hash__否则把对象放进集合或字典时可能因为哈希不一致而出现奇怪问题。5.4 从其他语言转 Python 的开发者最容易犯的错如果你是 Java、C 转 Python最容易踩的坑就是把“引用比较”的习惯带过来。在 Java 里对于 Integer 包装类型比较的是引用只有equals()才比较值。很多人一转 Python想当然地以为也是比较引用结果用错了地方。在 Python 里正好相反是主流的值比较方式is才是那个负责“引用比较”的角色。所以如果你刚转过来记住这句口诀“要比较内容用 要判断是不是同一个对象用 is。”这句话能帮你省掉一大堆调试时间。如果非要用性能角度去解释为什么大家更常用Python 的在大多数内置类型上已经做了足够好的优化而且它的语义更适合业务逻辑。“值相等”才是你在写业务时真正关心的至于“是不是同一个对象”那是语言底层实现关心的事。6. 我的几点实操心得最后分享几个我自己的使用习惯供你参考。第一凡是判断变量是不是None我永远写is None或is not None不要问为什么这就是 Python 社区的共识也是最安全的写法。第二在写自定义类时我会刻意设计好__eq__的语义并且在单元测试里同时验证和is的行为避免未来某个同事误用导致逻辑错误。第三调试内存相关的问题时我会频繁用id()和is验证对象是否真的被复用这比看引用计数更直观。另外给初学者一个建议不要在业务代码里依赖小整数缓存和字符串驻留的行为。这些机制是 CPython 的实现细节不是语言规范未来版本完全可能变化。你可以出于好奇去做实验但在生产代码里只依赖和is的规范语义才是稳妥的。身份运算符的内容看着不多但背后牵扯出的是 Python 的对象模型。把is搞明白了你对变量、对象、引用这些概念的理解会上一个台阶。这一章如果能彻底吃透后面学什么可变对象、不可变对象、深浅拷贝都会轻松很多。
返回列表