Python检测None必须采用is指令 封堵魔术方法重载隐患并降低开销

【python】为什么判断一个值是否为None的时候,一定要用is呢? — 码农高天

码农高天

布尔求值会误判空容器,而双等号易受类魔术方法重载干扰且调用链更重,只有使用is直接比较底层内存指针才能杜绝逻辑漏洞并获得最高执行效率。

AI-assisted summary of 【python】为什么判断一个值是否为None的时候,一定要用is呢? — original source. Check the original for full context.

Source published:

Key ideas

  • 使用 `if not a` 无法准确判断 None,因为空列表、空字典、空集合与布尔值 False 都会被隐式判定为假,极易引发业务状态误判。 (0:00)
  • `== None` 存在逻辑漏洞,因为等于运算符调用内部的 `__eq__` 魔术方法,可被自定义类重载导致判断失效。 (5:00)
  • 从 CPython 字节码与源码实现来看,`is None` 对应 `IS_OP` 指令直接进行 C 级指针地址比较,而 `== None` 触发 `COMPARE_OP` 及富比较调用链,在性能与安全性上均劣于前者。 (5:00)

Detailed notes

## Background 该视频由技术博主“码农高天”于2023年9月27日发布在YouTube平台上。视频承接上期留下的悬念,主要探讨在Python中判断一个变量是否为None时,为何必须使用“is”而不是“==”或隐式布尔判断等其他方式。

## TL;DR 高天首先通过空列表、空字典等内置对象及重载__bool__的类,说明了直接使用if a或if not a容易误判真值,并通过斐波那契数列共享缓存失效的实例揭示了业务隐患。随后他分析了使用== None的缺陷,指出其依赖可被重载的__eq__方法,存在被篡改逻辑的风险。最后,高天借助dis反编译模块与CPython源码,对比了IS_OP与COMPARE_OP指令的底层实现,论证了is属于C层面的轻量指针比较,兼具无可匹敌的安全性和执行效率。

## Atmosphere & style 视频展现了硬核且严谨的极客技术分享风格,高天的讲解条理清晰、从容不迫,既有实战踩坑的幽默调侃,又有源码级别的深度剖析。整个过程伴随现场代码演示与反编译指令对比,由浅入深地打破常见直觉误区,带给观众一种豁然开朗的透彻感与踏实感。

## Bulletin - [00:00](https://www.youtube.com/watch?v=pDhUWOj_a0M&t=0s) 使用 `if not a` 无法准确判断 None,因为空列表、空字典、空集合与布尔值 False 都会被隐式判定为假,极易引发业务状态误判。 - [05:00](https://www.youtube.com/watch?v=pDhUWOj_a0M&t=300s) `== None` 存在逻辑漏洞,因为等于运算符调用内部的 `__eq__` 魔术方法,可被自定义类重载导致判断失效。 - [05:00](https://www.youtube.com/watch?v=pDhUWOj_a0M&t=300s) 从 CPython 字节码与源码实现来看,`is None` 对应 `IS_OP` 指令直接进行 C 级指针地址比较,而 `== None` 触发 `COMPARE_OP` 及富比较调用链,在性能与安全性上均劣于前者。

## Key takeaways - [00:00](https://www.youtube.com/watch?v=pDhUWOj_a0M&t=0s) 编程应遵循“永远不要相信输入数据”(Don't trust the user)原则,判断 None 必须确保在任何输入下都稳健有效。 - [00:00](https://www.youtube.com/watch?v=pDhUWOj_a0M&t=0s) `if a` 或 `if not a` 本质是将变量作为布尔值求值,类似 C 语言空指针判断,但在 Python 中多类对象都会求值为 False。 - [00:00](https://www.youtube.com/watch?v=pDhUWOj_a0M&t=0s) Python 内置的空列表 `[]`、空字典 `{}`、空集合 `set()`、`None` 以及 `False` 在布尔求值下均会被判定为假。 - [00:00](https://www.youtube.com/watch?v=pDhUWOj_a0M&t=0s) 在斐波那契缓存案例中,使用 `if cache:` 会将外部传入的初始空字典误判为未传参,导致各实例重新创建独立字典并破坏共享缓存逻辑。 - [00:00](https://www.youtube.com/watch?v=pDhUWOj_a0M&t=0s) 自定义类可以通过重载 `__bool__` 魔术方法改变真值测试行为,依赖布尔判断极易受到第三方库或自定义逻辑的干扰。 - [05:00](https://www.youtube.com/watch?v=pDhUWOj_a0M&t=300s) `==` 运算符依赖对象的 `__eq__` 魔术方法,若该方法被重载(例如恒返回 True),`a == None` 将产生完全错误的判定结果。 - [05:00](https://www.youtube.com/watch?v=pDhUWOj_a0M&t=300s) 通过 `dis` 模块反编译可知,`a is None` 编译为 `IS_OP` 字节码指令,而 `a == None` 编译为 `COMPARE_OP` 指令。 - [05:00](https://www.youtube.com/watch?v=pDhUWOj_a0M&t=300s) 在 CPython 源码中,`IS_OP` 仅执行 C 语言层面的对象指针比较(`left == right`),开销极小且速度极快。 - [05:00](https://www.youtube.com/watch?v=pDhUWOj_a0M&t=300s) `COMPARE_OP` 底层走 `PyObject_RichCompare` 与 `do_richcompare` 流程,可能涉及动态派发和 Python 层面魔术方法的函数调用开销。 - [05:00](https://www.youtube.com/watch?v=pDhUWOj_a0M&t=300s) `COMPARE_OP` 的执行速度永远慢于 `IS_OP`,使用 `== None` 毫无性能收益且伴随重载风险,应始终使用 `is None` 或 `is not None`。

## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=pDhUWOj_a0M&t=0s) 为什么不能用 `if a` 或 `if not a` 判断 None

码农高天在上一期视频留了一个悬念,提到在 Python 中判断一个值是否为 `None` 时一定要使用 `is None`,引发了评论区不少关于“为什么”以及“可以用其他方法判断”的讨论。在屏幕左侧,高天列出了三种常见的判断方法(包括布尔求值、`==` 和 `is`),尽管在变量确实是 `None` 的情况下它们都能得到看似正确的结果,但在实际开发中必须遵循一条原则:“在编程的时候有一句俗话叫 Don't trust the user,就是永远不要相信你输入的数据。”代码应当追求在任何可能的输入情况下都能稳健且正确地工作。

第一种常见的写法是直接写 `if a`(或者 `if not a`)。这种写法的本质是将变量 `a` 当作布尔值(boolean)来求值,逻辑类似于 C 语言中判断空指针。然而在 Python 中,求值为 `False` 的对象远不止 `None` 一个。高天给出了一个包含五个元素的列表:一个空列表 `[]`、一个空字典 `{}`、一个空集合 `set()`、`None` 和 `False`。通过遍历并打印 `if not a` 为真的项,会发现这五个东西全部都会被判定为 `False`。如果以此来判断 `None`,就会把空容器和布尔假值全部误判。

为了说明这种误判在真实业务中带来的严重隐患,高天展示了一个带缓存的斐波那契数列计算器类 `FibCalculator`。在 `__init__` 初始化函数中,传入了一个可选的 `cache` 参数:代码逻辑本意是“如果调用者没有传 `cache`(即为 `None`),就初始化一个空字典,否则就使用外部传入的 `cache`”。然而作者在代码中写成了 `if cache:`。接着主程序创建了一个空的字典 `cache` 并共享传入两个独立的计算器实例,分别去计算 `fib(10)`。如果去统计实际没有命中缓存(走入 `else` 分支重新计算)的次数,就会发现缓存根本没有在两者之间共享。原因正是初始化时传入的 `cache` 是一个空字典 `{}`,在 `if cache` 判断中被判定为假,导致两个实例都各自重新初始化了一个全新的本地字典,彻底破坏了原本共享缓存的设计。

除了内置数据结构在为空时会求值为 `False` 之外,Python 用户自定义的类还可以重载 `__bool__` 这个魔术方法(magic method)。高天随手写了一个类并将它的 `__bool__` 方法硬编码返回 `False`,导致该对象在任何 `if not a` 的判断下都会被当作假。尽管工作中的代码不会写得这么无厘头,但很多第三方库和自定义数据结构确实会对 `__bool__` 进行重载,完全依赖布尔判定来当作 `None` 检查必然会导致难以排查的 Bug。

- 在 Python 中被隐式判定为 `False` 的常见内置对象包括:空列表 `[]`、空字典 `{}`、空集合 `set()`、`None` 以及布尔值 `False`。 - 斐波那契缓存案例展示了当传入空字典 `{}` 作为初始共享缓存时,`if cache:` 判定失败导致分别开辟独立字典,共享逻辑失效。 - 自定义类可以通过实现 `__bool__()` 魔术方法来自定义真值测试(truth value testing)。

```mermaid flowchart TD accTitle: Python中布尔判断None的缺陷 accDescr: 展示传入空字典时if not cache被误判为None并导致缓存无法共享的逻辑流程 subgraph Input["输入对象 input"] NoneVal["None"] EmptyDict["{} (空字典)"] EmptyList["[] (空列表)"] end

subgraph Logic["if not a 判断分支"] Eval{"evaluates to False?"} end

subgraph Consequence["执行后果"] BranchTrue["均进入判断为假的逻辑分支"] Bug["意外创建新字典,破坏外部缓存共享"] end

NoneVal --> Eval EmptyDict --> Eval EmptyList --> Eval Eval -->|"全部判定为 True"| BranchTrue BranchTrue --> Bug ```

---

[05:00](https://www.youtube.com/watch?v=pDhUWOj_a0M&t=300s) `== None` 的重载漏洞与字节码层面的指针比较优势

既然布尔判定不可靠,那么使用 `if a == None` 会不会好一些?高天用包含空容器、`None` 和 `False` 的测试列表再次测试,发现 `a == None` 确实精准地只挑出了 `None`。但它依然不是一个合格的替代方案,因为 `==` 在 Python 内部调用的是 `__eq__` 魔术方法,这意味着等于运算符也是可以被用户自定义类任意重载的。高天现场演示了一个将 `__eq__` 永远返回 `True` 的自定义类,其实例在进行 `a == None` 判断时直接返回了真。

面对“没人会写这么愚蠢代码”的质疑,高天反驳道,开发者既无法预料别人写出什么样的代码,也无法预料其重载运算符背后的动机。即便不考虑 `__eq__` 被恶意或意外重载的问题,从 Python 底层的字节码(bytecode)来看,`==` 与 `is` 也有着本质区别。高天使用 `dis` 模块对 `a == None` 和 `a is None` 进行了反编译对比: - `a == None` 被编译为 `COMPARE_OP` 指令; - `a is None` 被编译为 `IS_OP` 指令。

通过翻阅 CPython 源码(视频以 CPython 3.9 为例,注最新版本中已被重构为宏,但底层逻辑一致),高天对比了两个操作的 C 语言实现: `IS_OP` 在底层直接比较两个对象的内存指针(`left == right`),这在 C 语言中是一个极度轻量且极快的指针数值比较。而 `COMPARE_OP` 内部走的是 `PyObject_RichCompare`(富比较),最终会落到 `do_richcompare` 函数中。`do_richcompare` 的调用链远比指针比较复杂:如果对象没有定义富比较方法且是内置数据类型还好,一旦定义了或者需要动态派发,它必须在底层再次调用 Python 层面的魔术方法(如 `__eq__`),这种函数调用的开销要大得多。

高天总结道:“换言之,`COMPARE_OP` 永远会比 `IS_OP` 要慢,至于慢多少,要看你这个 object 有没有自定义的比较函数。”尽管在大部分日常业务代码中微秒级的速度差异并不明显,但使用 `==` 等于白白牺牲了性能却没有任何收益,极端情况下还会因为运算符重载引发 Bug。“所以说以后任何时候当你想判断一个值是不是 None 的时候,直接用 `is` 或者 `is not`,这样就能保证你的程序起码在这个部分是不会出问题的。”

- 字节码指令对比:`a == None` 对应 `COMPARE_OP`,而 `a is None` 对应 `IS_OP`。 - 底层 CPython 实现:`IS_OP` 仅做 C 级别的对象指针地址比较(pointer comparison);`COMPARE_OP` 必须进入复杂的 `do_richcompare` 调用流程并可能触发 Python 函数调用。 - 安全性与性能权衡:使用 `==` 既面临 `__eq__` 被重载导致误判的风险,又承担了额外的开销,属于毫无收益的写法。

## Notable quotes > [05:00](https://www.youtube.com/watch?v=pDhUWOj_a0M&t=300s) 以后任何时候当你想判断一个值是不是 None 的时候,直接用 is 或者 is not,这样就能保证你的程序起码在这个部分是不会出问题的。 — 码农高天

> [05:00](https://www.youtube.com/watch?v=pDhUWOj_a0M&t=300s) COMPARE_OP 永远会比 IS_OP 要慢,至于慢多少,要看你这个 object 有没有自定义的比较函数。 — 码农高天

## Mentioned 码农高天, Python, CPython, dis, C 语言

## Video description 这期视频把上一期留下的问题回答一下,聊一聊为什么判断None的时候,一定要用is,而不是其他的方法。

[Source](https://www.youtube.com/watch?v=pDhUWOj_a0M)

中文简报

Python检测None必须采用is指令 封堵魔术方法重载隐患并降低开销 布尔求值会误判空容器,而双等号易受类魔术方法重载干扰且调用链更重,只有使用is直接比较底层内存指针才能杜绝逻辑漏洞并获得最高执行效率。 高天首先通过空列表、空字典等内置对象及重载__bool__的类,说明了直接使用if a或if not a容易误判真值,并通过斐波那契数列共享缓存失效的实例揭示了业务隐患。随后他分析了使用== None的缺陷,指出其依赖可被重载的__eq__方法,存在被篡改逻辑的风险。最后,高天借助dis反编译模块与CPython源码,对比了IS_OP与COMPARE_OP指令的底层实现,论证了is属于C层面的轻量指针比较,兼具无可匹敌的安全性和执行效率。 使用 `if not a` 无法准确判断 None,因为空列表、空字典、空集合与布尔值 False 都会被隐式判定为假,极易引发业务状态误判。 (0:00) `== None` 存在逻辑漏洞,因为等于运算符调用内部的 `__eq__` 魔术方法,可被自定义类重载导致判断失效。 (5:00) 从 CPython 字节码与源码实现来看,`is None` 对应 `IS_OP` 指令直接进行 C 级指针地址比较,而 `== None` 触发 `COMPARE_OP` 及富比较调用链,在性能与安全性上均劣于前者。 (5:00)

Reading summary

码农高天指出,在 Python 中判断变量是否为 None 时必须坚持使用 `is None`,因为直接使用布尔求值或等于操作符 `==` 既存在逻辑误判与重载漏洞的风险,又会在底层带来不必要的性能开销。 布尔真值判断的隐式转换与逻辑漏洞 (0:00) 在 Python 中直接写 `if not a` 无法准确判断变量是否为 None,因为该写法的实质是布尔求值。除 None 以外,空列表 `[]`、空字典 `{}`、空集合 `set()` 以及布尔值 `False` 均会被判定为假。此外,自定义类还可以通过重载魔术方法 `__bool__` 返回 False,使得对象在真值测试中被误判。 高天以带缓存的斐波那契计算器 `FibCalculator` 为例说明此缺陷:开发者若使用 `if cache:` 来判断外部是否传入了缓存对象,在传入初始空字典 `{}` 时就会被判定为假。这会导致每个计算器实例分别开辟全新的本地字典,彻底破坏原本预期的跨实例共享缓存机制,从而引入难以排查的业务 Bug。 等于运算符重载带来的潜在安全隐患 (5:00) 虽然使用 `if a == None` 能在常规测试中排除空容器和 False 的干扰,但它依然不是安全的替代方案。`==` 在 Python 内部会调用 `__eq__` 魔术方法,这意味着等于比较的逻辑可以被用户自定义类任意重载。高天演示了将 `__eq__` 硬编码返回 True 的类,其实例在进行 `a == None` 判断时会直接返回真。 面对“没人会故意这样写”的质疑,高天反驳称开发者永远无法预料输入数据的具体类型以及第三方库重载运算符的动机。遵循“永远不要相信输入数据”(Don't trust the user)的编程原则,代码必须防范因对象重载 `__eq__` 而引发的非预期行为。 底层字节码机制与指针比较的性能差异 (5:00) 从 CPython 底层实现来看,`a == None` 和 `a is None` 对应的字节码指令截然不同。通过 `dis` 模块反编译可知,`a is None` 编译为 `IS_OP`,而 `a == None` 编译为 `COMPARE_OP`。在以 CPython 3.9 为例的底层 C 语言代码中,`IS_OP` 仅执行轻量的指针数值比较(`left == right`),速度极快。 相比之下,`COMPARE_OP` 会进入 `PyObject_RichCompare` 和 `do_richcompare` 的复杂调用链。一旦涉及动态派发或自定义方法,还必须在底层重新调用 Python 层面的 `__eq__` 函数。高天指出,`COMPARE_OP` 的开销注定高于 `IS_OP`,使用 `==` 等于在没有任何收益的前提下白白牺牲了性能。 高天总结认为,尽管日常业务中微秒级的速度差异并不明显,但使用 `==` 或隐式布尔判断不仅损耗性能,还可能因魔术方法重载埋下隐患;因此在任何场景下判断变量是否为 None,都应一律采用 `is` 或 `is not` 以确保代码的稳健与正确。

Source quotes

[5:00] 码农高天: 以后任何时候当你想判断一个值是不是 None 的时候,直接用 is 或者 is not,这样就能保证你的程序起码在这个部分是不会出问题的。 [5:00] 码农高天: COMPARE_OP 永远会比 IS_OP 要慢,至于慢多少,要看你这个 object 有没有自定义的比较函数。

Original transcript

[0:00] 在上一期视频里我留了一个扣我说在判断是不是none的时候一定要用isnone那有些评论就会说为什么对不对那也有些人反对说我还有其他很多判断是不是NUN的方法今天我们就来讲一下为什么判断NUN的时候一定要用is好 我们看我在左边呢 列出了三种比较常见的啊 不管是对还是不对的 判断NAND的方法在Pathom里面 那么我们尝试运行一下啊 尝试运行一下我们会发现呢,这三种方法在A试乱的时候都正确的判断了A是不是那这句话有点绕啊 自己理解一下那既然如此 为什么我会说在判断是不是none的时候一定要用ease那在我们编程的时候呢有一句俗话叫 don't trust the user就是永远不要相信你输入的数据当然这个话比较极端啊但是大概的方向永远是尽可能的在任何输入的情况下都正确工作我们先来说第一种写法 if a那if a本质上是把a当成一个boolean来进行计算 这个写法其实有点像c里面对指针的判断但是我们知道在python里面把它当作boolean evaluate 成false的可不止是none我们举几个例子我们看在左边我给出了一个listlist里面有5个东西分别是一个空的list一个空的dictionary一个空的setnone和false我们把这五个东西都遍了一遍如果if not a就是如果它是false的话我们把它打印出来运行一下谢谢大家我们发现这五个东西都会被当成false所以当你用if a或者if not a去判断a是不是non的时候 你很有可能会错误的判断了这些情况那有的人可能觉得了这是无所谓的对不对他们几个都相当于是没有吗他有可能会出什么错误呢只要我输入不乱给不就没事吗我们看个例子好 我们看左边这段程序 这段程序稍微有点长 但不是那么复杂我做了一个菲波那切的计算器然后在初始化的时候传进了一个cache这个cache如果是none的话呢就让它初始化成一个dictionary否则的话就要这个cache然后费波纳切是跟原来一样只不过我的费波纳切加了一个cash这样就不会出现没有意义的抵规了就每次抵规他要记录一下这个程序本身可能多多少少有点蠢但是我们在平时的编程中还是会遇到这种共享一个数据结构的事儿的好 那我们看啊 我们初始化了一个cache 然后把这个cache同时给了两个fib calculator然后我们做两个Fib10的运算谢谢大家我们看到结果是对的办事我们思考一下这个cash被共享了吗我们在else这个case里面打印一下n也就是每一次没有用到cache的时候都打印一下n我们会发现实际上Cash并没有被他们两个之间贡献那么原因当然了前面已经提到了对不对就在这儿因为这里的if cache除了在cache是none的时候不成立之外在cache是空的dictionary的时候也不成立那不仅如此 我们知道python的很多内置的术语结构在某些特殊情况下会evaluate成false而用户自定义的数据结构通过对boe这个magic method的重载也可以自定义它什么时候是true什么时候是false比如说我写的这个最简单这么一个class我让它永远是false我们可以看到下面这个部分的代码自定一个object 然后用这个方法来判断他会认为他是一个Nun当然实际工作中的代表不会这么无厘头啊但是确实有一些数据结构会对Do这个函数进行重载导致你对Nan的判断出现问题好 那如果if A这种结构不行if A等于等于那有没有好一些呢我们看还是刚才那五个数据这次我们不用if not a了我们用if a等于等于看看结果如何谢谢大家这次看起来很好 只有NAN被挑出来了那if a等于等于是不是和if a is同样好的方法呢如果你还记得我们一分钟之前刚刚讲的内容的话你就会发现等于等于这个东西在python里依然是可以被重载的所以它跟刚才那个不面临的是同一个问题我们看通过对一个这个函数的重载我们如果让他永远return的话左边这个if a等于等于那样的判断他也会是真的你可能会说了 [5:00] 没有人会写这么愚蠢的代码对不对首先你不知道别人会不会写类似这样的代码你也不知道他为什么或者为什么不写这样的代码其次即便是没有这个ECO的重载用等于等于就和用is一样好了吗又到了字节码的时间了我们用this这个module 把a==n和aisn的compile的结果显示出来可以看到他们两个的差别就在这个Compare up和这个is option看起来都是一个bycode但是他们两个一样吗好 我们看这里是easeup的cpython的原码注意一下我这里用的是3.9版本最新的版本里面这个地方被换成了一个macro 但是意思是一样的我们看Ease的比较是直接比较两个object的地址。这里是一个指针比较而这个比较在C里面是非常快的它只需要比较两个指针就可以了那我们再看看 compare up其他的地方几乎都是一样的只是比较的部分他用了rich compare那reach compare最后会到哪呢Reach compare最后会落到这个函数里Do rich compare 肉眼可见的就比纸真的比较稍微复杂一些啊如果你参与比较的这个值是一个内置的数据结构或者是一个没有定义rich compare就是equal or not equal这些magic method object的话还好一些否则的话他还会再去调用一个python的函数这个钓友可就慢了换言之Compare up永远会比Eats up要慢至于慢多少 要看你这个object有没有自定义的比较的函数尽管在大部分的应用中这个差别其实并不明显但是在这里 我们牺牲掉了这一微小的速度 没有画回来任何东西 甚至在有些极端情况下 它还不好使所以说以后任何时候当你想判断一个值是不是Nun的时候直接用 is 或者 is not这样就能保证你的程序起码在这个部分是不会出问题的好 这期视频就到这里 感谢大家收看如果喜欢的话 记得点一下关注一键三连哦字幕志愿者 杨茜茜