判 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 with
isoris 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] -> False1和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 -> Falseis 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 FalseFalse、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:’
八、写法清单
| 场景 | 别写 | 写 |
|---|---|---|
| 判 None | x == None | x is None |
| 判非 None | x != None | x is not None |
| 判非 None(可读性别扭) | not x is None | x is not None |
| 参数"没传" | if not timeout: | if timeout is None: |
| 默认参数哨兵 | 用0/""当默认值 | 默认None,进来先判is None |
| 缓存里可能有 None | 只看d.get(k) | 加k in d或哨兵对象 |
| 只想判"空/假" | x is None | not 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,判相等才用==。