news 2026/10/3 6:45:47

判 None,为什么必须用 `is`

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
判 None,为什么必须用 `is`

判 None,为什么必须用is

写作约定:本文所有代码都在真实解释器上跑过,输出是从终端直接复制的。默认环境Python 3.14.7(numpy 2.4.4 / pandas 3.0.2);涉及版本差异和性能数字的地方会单独标出实测版本。凡是没实测验证的说法,本文不写。为省篇幅,个别处只贴相关几行,完整输出可用脚本重跑核对。

配套代码:

  • verify/01~10—— 每节结论的验证脚本,可直接重跑;08是跨版本对照,10把正文每个代码块原样跑一遍用来核对输出
  • examples/cache_and_optional.py—— 两个非用is None不可的实战场景

一、结论先给

PEP 8 原文(https://peps.python.org/pep-0008/,Programming Recommendations 一节):

Comparisons to singletons like None should always be done withisoris not, never the equality operators.

翻译成人话:和 None 比,永远写is None/is not None,别写== None。

ifxisNone:# 对...ifxisnotNone:# 对...ifx==None:# 错(能用,但会出问题)...

== None不是"语法错误",也不是"总是错的"。它是一个看起来能用、遇到特定对象就悄悄出错的写法。下面用实例说明它会怎么坏。


二、is和==比的不是一回事

  • is:两个名字指向的是同一个对象吗?(身份)
  • ==:两个对象值相等吗?(会调用__eq__)
i,f=1,1.0print("1 == 1.0 ->",i==f)print("1 is 1.0 ->",iisf)print("[1, 2] == [1, 2] ->",[1,2]==[1,2])print("[1, 2] is [1, 2] ->",[1,2]is[1,2])
1 == 1.0 -> True 1 is 1.0 -> False [1, 2] == [1, 2] -> True [1, 2] is [1, 2] -> False

1和1.0值相等但不是同一个对象;两个[1, 2]是两次新建的列表,内容相同、身份不同。所以平时不能拿is比较值。

那为什么 None 例外?因为 None 有单例保证。


三、None 是单例,所以is才可靠

整个进程里只有一个 None 对象。官方文档(https://docs.python.org/3/library/constants.html)的原话是:

None is the sole instance of the NoneType type.

所以你在任何地方拿到的 None 都是它:

classHolder:passa=Holder()a.value=Noneb=Holder()b.value=Noneimportcopyimportpickleprint("None is None ->",NoneisNone)print("a.value is b.value ->",a.valueisb.value)# 两个不同对象上的 None 属性print("type(None) ->",type(None))print("id(None) == id(a.value) ->",id(None)==id(a.value))print("type(None)() is None ->",type(None)()isNone)# 显式构造,还是同一个print("copy.deepcopy(None) is None ->",copy.deepcopy(None)isNone)print("pickle 往返后 is None ->",pickle.loads(pickle.dumps(None))isNone)
None is None -> True a.value is b.value -> True type(None) -> <class 'NoneType'> id(None) == id(a.value) -> True type(None)() is None -> True copy.deepcopy(None) is None -> True pickle 往返后 is None -> True

连type(None)()、深拷贝、pickle 往返都还是同一个对象。所以判断"是不是 None"这件事,身份判断和值判断在这个对象上恰好等价,is既准确又不用调用任何用户代码。

对比一下is用在数字/字符串上为什么不靠谱(都是 CPython 的实现细节,不能依赖):

small_a,small_b=256,256big_a,big_b=257,257print("256 is 256(同一个代码对象里的常量) ->",small_aissmall_b)print("257 is 257(同一个代码对象里的常量) ->",big_aisbig_b)print('int("257") is int("257")(运行时各造一个) ->',int("257")isint("257"))print('"abc" is "".join(["a","b","c"]) ->',"abc"is"".join(["a","b","c"]))print('"abc" == "".join(["a","b","c"]) ->',"abc"=="".join(["a","b","c"]))
256 is 256(同一个代码对象里的常量) -> True 257 is 257(同一个代码对象里的常量) -> True int("257") is int("257")(运行时各造一个) -> False "abc" is "".join(["a","b","c"]) -> False "abc" == "".join(["a","b","c"]) -> True

两个257是True(编译期常量折叠),int("257")两次却是False—— 同一个表达式换个写法结果就变。这正是"is只能用在单例上"的反面教材。


四、四个实例:x == None会怎么坏

4.1__eq__没考虑对方是 None,直接抛异常

这是最常见的一类报错。类里写了__eq__,但它假设other一定有某个属性:

classMoney:def__init__(self,cents):self.cents=centsdef__eq__(self,other):returnself.cents==other.cents m=Money(100)try:print("Money(100) == None ->",m==None)exceptAttributeErrorase:print("Money(100) == None -> AttributeError:",e)print("Money(100) is None ->",misNone)
Money(100) == None -> AttributeError: 'NoneType' object has no attribute 'cents' Money(100) is None -> False

is None只是比一下身份,根本不会去调__eq__,自然也不会炸。

4.2 有对象的__eq__对谁都返回 True

unittest.mock.ANY就是干这个的:它代表"随便什么都行",所以它和 None 也"相等"。

fromunittest.mockimportANYprint("ANY == None ->",ANY==None)print("ANY is None ->",ANYisNone)
ANY == None -> True ANY is None -> False

自己写一个"和谁都相等"的对象也一样:

classAnything:def__eq__(self,other):returnTrueobj=Anything()print("Anything() == None ->",obj==None)print("Anything() is None ->",objisNone)
Anything() == None -> True Anything() is None -> False

这类对象混进业务代码里(mock、哨兵、代理对象),x == None就会把"不是 None"误判成 None。

4.3 numpy / pandas:== None返回的不是布尔值

这一条最实用。数组和 Series 的==是逐元素比较,结果是一整个数组:

importnumpyasnp arr=np.array([1,2,3])print("arr == None ->",repr(arr==None))print("arr is None ->",arrisNone)print("type(arr == None) ->",type(arr==None).__name__)try:ifarr==None:passexceptValueErrorase:print("if arr == None -> ValueError:",e)
arr == None -> array([False, False, False]) arr is None -> False type(arr == None) -> ndarray if arr == None -> ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()

pandas Series 的坑更深:即使某个元素真的是 None,s == None也全是 False。

importpandasaspd s=pd.Series([1,None,3],dtype=object)print("s[1] is None ->",s[1]isNone)print("(s == None).tolist() ->",(s==None).tolist())print("s.isna().tolist() ->",s.isna().tolist())try:ifs==None:passexceptValueErrorase:print("if s == None -> ValueError:",e)print("s is None ->",sisNone)
s[1] is None -> True (s == None).tolist() -> [False, False, False] s.isna().tolist() -> [False, True, False] if s == None -> ValueError: The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all(). s is None -> False

注意中间两行:元素is None是 True,== None却给 False。想找缺失值要用s.isna(),不要用== None。(s == None全 False 这一点在 pandas 2.2.2 + numpy 1.26.4 和 pandas 3.0.2 + numpy 2.4.4 上实测相同。)

而s is None只是判断"这个变量本身是不是 None",返回False,干净利落。

4.4 小结

写法做了什么风险
x is None比一次身份无,不调用任何用户代码
x == None调__eq__可能抛异常、可能误判、可能返回非布尔值

五、别和if not x混了

not x为真只说明 x 是"假值",假值远不止 None:

值 bool(值) not 值 值 is None None False True True False False True False 0 False True False 0.0 False True False '' False True False '0' True False False [] False True False [0] True False False {} False True False set() False True False () False True False nan True False False

False、0、0.0、''、[]、{}、()都满足not x,但它们都不是 None。所以:

  • 只想判"是不是 None" →x is None
  • 想判"空/假" →not x

两者不能互相替换。最容易踩的场景是可选参数:

defrequest(url,retries=None,timeout=None):ifretriesisNone:retries=3iftimeoutisNone:timeout=10ifnotisinstance(retries,int):raiseTypeError("retries 必须是 int")return"GET %s (retries=%d, timeout=%ss)"%(url,retries,timeout)print(request("https://example.com"))print(request("https://example.com",retries=0))# 0 = 不重试,合法输入print(request("https://example.com",timeout=0.5))# 0.5 也不能被当成"没传"
GET https://example.com (retries=3, timeout=10s) GET https://example.com (retries=0, timeout=10s) GET https://example.com (retries=3, timeout=0.5s)

retries=0(不重试)、timeout=0.5都是合法输入。如果这里写成if not retries:,传 0 就会被当成"没传",行为直接错了 —— 这是is None最值钱的用法。

另一类:缓存里存过 None 时,dict.get()分不清"没命中"和"命中的值是 None",得用key in d或哨兵对象来区分(见examples/cache_and_optional.py)。


六、速度:实测数字,但它不是核心理由

网上常说"is比==快",实测确实快,但别把它当主要理由。用timeit直接测比较这一步(不套函数调用),本机 Python 3.14.7:

x = None is: 5.62 ns | ==: 8.73 ns | ==/is = 1.6x x = object() is: 7.81 ns | ==: 16.63 ns | ==/is = 2.1x class C: def __eq__(self, other): is: 8.61 ns | ==: 47.47 ns | ==/is = 5.5x

第三行才是重点:一旦对象带 Python 层的__eq__,== None要真的把那段代码跑起来,慢 5 倍以上,还会带上它的副作用。对着 None 本身比,两者只差 1.5 倍左右,几纳秒的事。

这个数字很不稳定:同一台机器重复跑会漂移,跨版本 3.8 / 3.12 / 3.13 / 3.14 测得的倍率在 1.3x ~ 1.6x 之间。几纳秒不值得作为设计理由,语义正确才值得。

顺带看一眼字节码,能看出两者根本不是一类操作(下面省掉了行号和RESUME):

--- dis: x is None --- LOAD_FAST_BORROW 0 (x) LOAD_CONST 0 (None) IS_OP 0 (is) <- 专门的身份比较指令 RETURN_VALUE --- dis: x == None --- LOAD_FAST_BORROW 0 (x) LOAD_CONST 0 (None) COMPARE_OP 72 (==) <- 通用比较,会走富比较协议 RETURN_VALUE

七、语言和工具给的两个信号

信号一:is配字面量会被警告。实测 3.8 / 3.12 / 3.13 / 3.14 上,x is 1、x is "a"都会抛SyntaxWarning(3.7 没有);而x is None不会。

x is None -> OK x is not None -> OK x == None -> OK x is 1 -> OK ['SyntaxWarning: "is" with \'int\' literal. Did you mean "=="?'] x is "a" -> OK ['SyntaxWarning: "is" with \'str\' literal. Did you mean "=="?'] x is [] -> OK

最后一行的x is []值得单独说:它不报警告,但永远是 False—— 每次执行[]都新建一个列表,身份不可能相同。is只留给 None/True/False 这类单例,别用在字面量上。

各版本对照(verify/08_version_matrix.py的完整输出里,这里只截了版本和警告两列):

| Python 3.7.8rc1 | x is 1 警告: - | Python 3.8.10 | x is 1 警告: SyntaxWarning | Python 3.12.10 | x is 1 警告: SyntaxWarning | Python 3.13.3 | x is 1 警告: SyntaxWarning | Python 3.14.7 | x is 1 警告: SyntaxWarning

(顺带:本机实测 3.7 / 3.8 把x is None编译成COMPARE_OP,3.12 / 3.13 / 3.14 是IS_OP;中间的 3.9 ~ 3.11 没在本机实测,就不下结论了。这只是实现细节,写代码不用管。)

信号二:lint 工具会直接拦。pycodestyle 的错误码E711原文就是(flake8 就是在 pycodestyle 外面包了一层):

E711: comparison to None should be ‘if cond is None:’


八、写法清单

场景别写写
判 Nonex == Nonex is None
判非 Nonex != Nonex is not None
判非 None(可读性别扭)not x is Nonex is not None
参数"没传"if not timeout:if timeout is None:
默认参数哨兵用0/""当默认值默认None,进来先判is None
缓存里可能有 None只看d.get(k)加k in d或哨兵对象
只想判"空/假"x is Nonenot x

not x is None语法上没错(等价于not (x is None)),但读起来容易看漏一个not,统一写x is not None更好。


九、一句话总结

is None不调用对象的任何代码,而 None 又是语言保证的单例,所以它既准确又安全;== None会把判断权交给对方的__eq__,遇到自定义__eq__、mock 对象、numpy/pandas 就会抛异常或给出反直觉的结果。判 None 用is,判空值用not,判相等才用==。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 6:45:43

零基础入门ESP32蓝牙BLE:MicroPython从环境搭建到手机控制LED

1. 为什么我建议零基础从蓝牙BLE切入ESP32很多人拿到ESP32开发板的第一反应是连WiFi、点灯、跑个Web服务器。但如果你问我&#xff0c;零基础入门ESP32最应该先玩什么&#xff0c;我会毫不犹豫地说&#xff1a;蓝牙BLE。原因很直接——它不需要路由器、不需要配网、不需要知道I…

作者头像 李华
网站建设 2026/10/3 6:45:43

嵌入式偶发Bug排查实战:串口、蓝牙、烧录的换机排除与批次对照

1. 偶发Bug的排查哲学&#xff1a;为什么"换一台试试"是最被低估的调试手段做嵌入式这行十几年&#xff0c;我最怕的不是那种一上电就冒烟的硬故障&#xff0c;而是那种跑三天才出现一次、复位之后又一切正常的偶发问题。串口丢包、蓝牙掉线、烧录校验失败——这三类…

作者头像 李华
网站建设 2026/10/3 6:45:27

数字递增滚动功能如何做

首页「满意度 98%、项目数 120」这类指标&#xff0c;一般不会一进来就定格&#xff0c;而是页面滚到这里时从 0 往上加。本例用 jQuery 判断是否进入可视区&#xff0c;再用定时器改文字。演示页上方留了一段空白&#xff0c;方便往下滚看效果。 递增数字在页面上如何展示 数字…

作者头像 李华
网站建设 2026/10/3 6:44:34

SNMP+MQTT双协议组合:智能制造设备统一接入实践

大概是2019年&#xff0c;我接手了一个智能制造车间改造项目&#xff0c;设备形态特别杂&#xff1a;网络机房里思科、华为、博科光交各有一批&#xff0c;产线上还挂着上百台485电表、几十个温湿度/水浸传感器&#xff0c;甚至还有几台需要随时调工艺参数的PLC。那段时间我一直…

作者头像 李华
网站建设 2026/10/3 6:43:46

规范的AI论文软件梯队划分(2026 终极指南)

基于综合性能、学术适配度、用户口碑和功能完整性&#xff0c;以下是当前主流AI论文写作工具的权威排名&#xff0c;按综合推荐指数从高到低排列&#xff0c;并标注核心优势与适用场景。&#x1f3c6; 第一梯队&#xff1a;全流程学术解决方案&#xff08;★★★★★&#xff0…

作者头像 李华