码农高天
布尔求值会误判空容器,而双等号易受类魔术方法重载干扰且调用链更重,只有使用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)