ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

257 is 257 为什么是 True?小整数缓存背后还藏着一个更容易被忽略的机制

257 is 257 为什么是 True?小整数缓存背后还藏着一个更容易被忽略的机制 「Python 进阶之路」系列 Day31写在前面模块六写完从今天开始进入模块七——面试高频题串讲接下来几篇不再引入全新知识点而是把前面容易被单独拎出来问、又特别容易感觉懂了但一深挖就露馅的话题重新过一遍。第一篇是is/和小整数缓存、字符串驻留——Day01 讲过基础概念今天要挖穿一个反直觉的现象257 is 257为什么会是True而 257 明明已经超出了 -5~256 的缓存范围。一、是什么两个幕后优化Day01 讲过is比较身份id()、比较值__eq__也提过 -5~256 的小整数缓存。今天重新拎出来讲透——不只是是什么而是深挖为什么有的地方is判断看起来符合直觉有的地方又不符合这背后是两套独立的机制在起作用小整数缓存CPython 对 -5 到 256 之间的整数提前创建好并常驻内存任何地方用到这个范围内的整数值都直接复用这些现成对象字符串驻留interning对看起来像标识符的字符串只包含字母、数字、下划线且不以数字开头CPython 会尝试复用已有的相同字符串对象而不是重新创建二、为什么省内存 加速比较这两个优化的动机是一致的整数和字符串在程序里被大量重复使用变量名、字典 key、小的计数值如果每次都创建新对象既浪费内存也让比较变慢。驻留之后比较可以先做一次廉价的id()比较命中就直接返回True不用再逐字符/逐位比较这只是实现里的一个内部优化不代表变成了is。范围和规则的设计也有取舍小整数缓存选 -5~256 是因为这个区间覆盖了绝大多数业务代码里常见的计数、索引、状态码字符串驻留只对标识符样式的字符串生效是因为源码里大量重复出现的字符串常量主要就是变量名、属性名、关键字参数名这一类把所有字符串都无条件驻留反而会增加不必要的内存和维护开销。三、怎么用1. 真正隔离出小整数缓存和字符串驻留这里有个容易被绕进去的坑如果两个字面量写在同一条语句/同一个函数里is判断会被编译器常量折叠干扰看不出真正的缓存边界。用exec()把两个值放进完全独立的代码对象才能验证纯粹的缓存/驻留效果defisolated_is(code_a,code_b):ns1,ns2{},{}exec(fv {code_a},ns1)exec(fv {code_b},ns2)returnns1[v]isns2[v]print(isolated_is(256,256))# True —— 在缓存范围内print(isolated_is(257,257))# False —— 超出缓存范围print(isolated_is(hello,hello))# True —— 标识符样式自动驻留print(isolated_is(hello world,hello world))# False —— 带空格不驻留print(isolated_is(hello!,hello!))# False —— 带特殊符号不驻留结果完全符合预期256 在缓存范围内是True257 超出范围是Falsehello这种标识符样式的字符串自动驻留是True带空格或特殊符号的字符串不驻留是False。2. 关键陷阱常量折叠和缓存是两回事是否是否两个整数字面量比较is是否在同一个代码对象里编译器做常量折叠大概率共享同一个对象值是否在负5到256之间小整数缓存共享同一个对象各自独立创建is通常为False如果不用exec()隔离直接在同一条语句里写两个超出缓存范围的整数会得到一个反直觉的结果c,d257,257print(cisd)# True 257明明超出了-5~256的缓存范围用dis看字节码就能看穿真相importdisdefsame_stmt():c,d257,257returncisd dis.dis(same_stmt)# LOAD_CONST 1 ((257, 257)) ← 编译器把 (257, 257) 直接折叠成了一个常量元组这里的True根本不是小整数缓存起的作用而是编译器在编译这个函数时发现257这个常量在同一个代码对象里出现了两次做了常量去重——两个257在字节码层面从一开始就是同一个对象。这个优化只发生在同一个代码对象同一个函数/模块编译期就能确定的常量上一旦这两个257来自运行时计算比如从函数参数、input()、数据库查询结果里来常量折叠完全不起作用is判断会老老实实地变成False。3. sys.intern()手动驻留如果确实需要让运行时拼接出来的字符串也享受驻留的好处比如高频比较同一批字符串、想用is加速比较可以用sys.intern()手动驻留importsys parthelzpartlo# 运行时拼接不会自动驻留yhelloprint(zisy)# Falsez2sys.intern(partlo)print(z2isy)# True —— 手动驻留后和字面量hello共享了同一个对象四、面试追问Q1is和的核心区别是什么is比较的是身份本质是比较两个对象的id()比较的是值调用的是对象的__eq__方法。判断值相等永远应该用判断是不是同一个对象尤其是判断None/True/False这类单例用is。Q2小整数缓存的范围是多少为什么要跨代码对象验证范围是 -5 到 256。如果两个整数字面量写在同一条语句或同一个函数里编译器的常量折叠会让结果看起来像缓存生效了哪怕数值已经超出了缓存范围必须用exec()之类的手段把两者放进完全独立的代码对象才能验证真实的缓存边界。Q3什么样的字符串会被自动驻留符合标识符格式的字符串——只包含字母、数字、下划线且不以数字开头比如hello。带空格、标点等特殊字符的字符串比如hello world、hello!默认不会被自动驻留。Q4为什么同一条语句里两个超出缓存范围的整数is判断却是True这是编译器的常量折叠/常量去重在起作用不是小整数缓存——编译器发现同一个代码对象里出现了两次相同的字面量常量会在字节码层面把它们直接合并成同一个常量对象用dis反汇编能直接看到这个折叠痕迹比如(257, 257)被编译成一个共享同一个 257 对象的常量元组。Q5这些行为能不能依赖用来做业务判断不能。小整数缓存、字符串驻留、常量折叠都是 CPython 的实现细节和性能优化不是 Python 语言规范承诺的行为不同版本、不同实现比如 PyPy可能表现不一样。判断值相等永远应该用不能依赖is在特定场景下碰巧表现出来的相等结果。下一篇预告Day32 讲全局变量与global/nonlocal——这两个关键字分别解决什么问题混用嵌套函数时容易踩的坑有哪些。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表