码农高天
AI-assisted summary of 【python】python中什么会被当真?你知道if判断背后的规则吗? — original source. Check the original for full context.
Source published:
Key ideas
- CPython 条件跳转字节码指令通过快速通道处理 True 与 False 单例,其他所有对象的布尔求值统一委托给底层核心函数 PyObject_IsTrue。 (0:00)
- Python 自定义类的真假判断严格遵循双层查找:优先调用 __bool__,若未定义则回退至 __len__;若两者均未定义,默认当作 True 处理。 (5:00)
- CPython 通过在底层强制校验 __bool__ 必须返回严格的 bool 类型、__len__ 必须返回合法长度数值,从机制上杜绝了递归求值死循环。 (5:00)
- 内置的 bool 本质是一个类(PyBool_Type)而非普通函数,在 if 语句中显式书写 bool(a) 与 if a: 在判断逻辑上完全等价且会带来冗余调用开销。 (10:00)
Detailed notes
## Background 该视频由开发者“码农高天”于 2024 年 1 月 25 日发布在 YouTube 平台上。视频简介指出,本期内容围绕 Python 中 `if` 条件判断背后的底层规则展开,探讨在 Python 这一灵活的语言中,任意类型的变量是如何被当作布尔值进行真假判定的。
## TL;DR 视频深度解析了 Python 中四种条件判断写法(`if a is True:`、`if a == True:`、`if a:` 与 `if bool(a):`)的机制差异与适用场景。通过翻阅 CPython 源码,主讲人展示了字节码指令 `POP_JUMP_IF_FALSE` 及核心求值函数 `PyObject_IsTrue` 如何对单例、数值与容器类型进行真假判断。针对自定义对象,视频拆解了优先调用 `__bool__`、次选 `__len__` 以及无定义时默认回退为真的协议与类型防御细节。最后指出 `bool` 本质是类而非普通函数,证明了在 `if` 语句中套用 `bool(a)` 是纯粹的冗余开销,并呼吁在工程中通过明确约束类型来提升代码健壮性。
## Atmosphere & style 整体氛围硬核严谨且条理清晰,兼具技术博主的从容与底层探究的求真感。主讲人直接带着观众逐行研读 CPython 源码与字节码,以抽丝剥茧的方式打破诸如“`bool` 是函数”或“自定义类走底部分支”等常见直觉误区。解说语调平实自信,在提供底层细节的同时频繁结合日常业务代码规范给出建议,给观众带来一种既通透又极具工程指导意义的学习体验。
## Bulletin - [00:00](https://www.youtube.com/watch?v=QxOdRkXpQwE&t=0s) CPython 条件跳转字节码指令通过快速通道处理 True 与 False 单例,其他所有对象的布尔求值统一委托给底层核心函数 PyObject_IsTrue。 - [05:00](https://www.youtube.com/watch?v=QxOdRkXpQwE&t=300s) Python 自定义类的真假判断严格遵循双层查找:优先调用 __bool__,若未定义则回退至 __len__;若两者均未定义,默认当作 True 处理。 - [05:00](https://www.youtube.com/watch?v=QxOdRkXpQwE&t=300s) CPython 通过在底层强制校验 __bool__ 必须返回严格的 bool 类型、__len__ 必须返回合法长度数值,从机制上杜绝了递归求值死循环。 - [10:00](https://www.youtube.com/watch?v=QxOdRkXpQwE&t=600s) 内置的 bool 本质是一个类(PyBool_Type)而非普通函数,在 if 语句中显式书写 bool(a) 与 if a: 在判断逻辑上完全等价且会带来冗余调用开销。
## Key takeaways - [00:00](https://www.youtube.com/watch?v=QxOdRkXpQwE&t=0s) 日常开发中常见的四种条件判断写法语义截然不同:if a is True 判断对象本体,if a == True 容易因操作符重载引发异常逻辑,而 if a: 与 if bool(a): 则是隐式与显式布尔求值。 - [00:00](https://www.youtube.com/watch?v=QxOdRkXpQwE&t=0s) 使用 if a is True 时其反面绝不仅是 a is False,在编写分支代码时必须警惕变量可能是除两个布尔单例之外的任意对象。 - [00:00](https://www.youtube.com/watch?v=QxOdRkXpQwE&t=0s) 在 if a: 生成的 POP_JUMP_IF_FALSE 字节码中,若操作数非 True/False,均会调用返回值为整型状态码(1 为真、0 为假、-1 为错误)的 PyObject_IsTrue。 - [00:00](https://www.youtube.com/watch?v=QxOdRkXpQwE&t=0s) PyObject_IsTrue 首先匹配单例:True 立即返回 1,False 和 None 则直接返回 0,这解释了为什么 None 在条件语句中总被视作假。 - [00:00](https://www.youtube.com/watch?v=QxOdRkXpQwE&t=0s) 数值类型的真假由其 tp_as_number 槽位中的 nb_bool 决定,判定规则为非零即真、等于零即假。 - [00:00](https://www.youtube.com/watch?v=QxOdRkXpQwE&t=0s) 容器类型的真值判断取决于其长度:dict 和 set 走 mapping 槽位,list、str 和 tuple 走 sequence 槽位,底层均以长度是否大于 0 来裁定。 - [05:00](https://www.youtube.com/watch?v=QxOdRkXpQwE&t=300s) 用 class 语法定义的 Python 自定义类具有非空的槽位指针,其真值裁定首先查找 __bool__,缺省时查找 __len__,二者皆无时默认返回真。 - [05:00](https://www.youtube.com/watch?v=QxOdRkXpQwE&t=300s) 为了规避底层无限递归,CPython 在第 6585 行显式通过 PyBool_Check 检查 __bool__ 的返回值,若不是严格的布尔类型会直接抛出 TypeError。 - [05:00](https://www.youtube.com/watch?v=QxOdRkXpQwE&t=300s) 在工程实践中应尽量压缩变量的类型范围,避免在变量可能是 None、0 或空列表的混合场景中滥用 if a:,因为空容器与缺失值往往需要不同的业务处理分支。 - [10:00](https://www.youtube.com/watch?v=QxOdRkXpQwE&t=600s) bool(a) 在 CPython 内部调用 PyBool_Type 的构造函数 tp_new,其内部同样是通过 PyObject_IsTrue 计算真伪后再由 PyBool_FromLong 构造布尔单例。 - [10:00](https://www.youtube.com/watch?v=QxOdRkXpQwE&t=600s) 在 if 判断中使用 if bool(a): 相比 if a: 没有任何语义或真值判定的改变,纯粹平添了一层函数/类型调用的开销。
## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=QxOdRkXpQwE&t=0s) 四种 if 条件判断的差异与 `if a:` 的字节码机制
在 Python 编程中,当我们想判断一个变量 `a` 时,常常会看到四种写法:`if a is True:`、`if a == True:`、`if a:` 以及 `if bool(a):`。如果把变量 `a` 赋值为 `True`,这四个语句都会成立并打印出四个 `True`;如果 `a` 赋值为 `False`,这四个语句则全都不成立,什么都不会打印。然而,一旦变量 `a` 是 `True` 和 `False` 之外的其他值,这四种写法的行为和底层逻辑就大相径庭。
```mermaid flowchart TD accTitle: Python 条件判断流程与分支路径 accDescr: 展示了从字节码到 PyObject_IsTrue 以及针对单例、内置类型与自定义对象的真值判定流程。
subgraph FastPath ["快速通道 (Fast Path)"] A["POP_JUMP_IF_FALSE 字节码"] --> B{"条件是否为单例?"} B -- "是 True / False" --> C["跳过函数调用直接跳转"] B -- "其他对象" --> D["调用 PyObject_IsTrue(v)"] end
subgraph BuiltinTypes ["内置类型分支"] D --> E{"是否为 True / False / None?"} E -- "True 返回 1" --> RET_TRUE["返回 1 (True)"] E -- "False / None 返回 0" --> RET_FALSE["返回 0 (False)"] E -- "否" --> F{"数值类型 (tp_as_number)?"} F -- "是" --> G["nb_bool(): 是否非零?"] G -- "非零" --> RET_TRUE G -- "等于 0" --> RET_FALSE F -- "否" --> H{"映射或序列类型?"} H -- "Mapping / Sequence" --> I["检查 length 是否大于 0"] I -- "> 0" --> RET_TRUE I -- "= 0" --> RET_FALSE end
subgraph CustomTypes ["Python 自定义类分支"] H -- "自定义类 (slot 非空)" --> J{"定义了 __bool__?"} J -- "是" --> K["调用 __bool__() (必须返回 bool)"] J -- "否" --> L{"定义了 __len__?"} L -- "是" --> M["调用 __len__() (必须返回有效长度)"] L -- "否" --> N["默认返回 1 (当成 True)"] K --> O["PyObject_IsTrue 检查返回值"] M --> O O --> P{"判断结果"} P -- "真 / 长度 > 0" --> RET_TRUE P -- "假 / 长度 == 0" --> RET_FALSE end ```
第一种写法 `if a is True:`,判断的是 `a` 是不是 `True` 本身。如果 `a` 是 `True` 之外的任何对象,哪怕是在布尔上下文中为真的对象,条件也绝不会通过。它适用于在多种数据种类中唯独想把 `True` 拿出来单独作为特殊 case 的场景。但码农高天强调,写代码时必须清楚“`a is True` 的反面不是 `a is False`”,因此在使用 `else` 时必须清醒地意识到: > “在我们写程序的时候,脑子里面一定要想着,这个变量它有可能是 True 和 False 之外的东西。”
对于第二种写法 `if a == True:`,高天直言“这个用法就基本上不应该出现了”。在 Python 中 `==` 运算符可以被重载,如果开发者真的想确认它是否为 `True` 本人,应该使用 `is`;使用 `==` 去判断一个自定义对象是否等于 `True`,其逻辑在绝大多数工程场景下都显得十分诡异且不合理。
第三种写法 `if a:` 是日常开发中的核心。通过查看 Python 字节码,会发现它的字节码非常简洁,核心指令为 `POP_JUMP_IF_FALSE`。翻看 CPython 源代码实现,该指令前有两个快速通道(fast path):若条件本身直接就是 `True` 或 `False`,便能省去一次函数调用直接跳转。如果不是这两个布尔单例,代码在第 3187 行调用核心函数 `PyObject_IsTrue`: > “实际上在 Python 内部进行 condition 计算的时候,它都是使用的这个 `PyObject_IsTrue` 函数。”
- `PyObject_IsTrue` 的返回值类型为 `int`,其中返回 `1` 代表 `True`,返回 `0` 代表 `False`,返回 `-1` 代表执行过程中发生错误。 - 函数首先检查最常见的内置单例:如果对象是 `True` 则返回 `1`;如果是 `False` 或者 `None` 则直接返回 `0`。这就从源码底层解释了为什么 `if None:` 会直接被当作 `False` 处理。 - 随后针对内置数值类型(如 `int`、`float`、`complex`、`decimal` 等),源码会查找其 `tp_as_number` 中的 `nb_bool` 函数。各种数值类型的规则是:如果等于 0 则返回 `False`,非零值则返回 `True`。 - 对于容器类型,源码分别检查 `tp_as_mapping` 与 `tp_as_sequence` 的长度函数。像键值对形式的 `dict` 以及基于哈希实现的 `set` 采用 mapping 的长度检测,而 `str`、`tuple`、`list` 等有序序列则采用 sequence 的长度检测。两者底层均通过判断容器长度(length)是否为 0 来决定:长度为 0 返回 `False`,非 0 则返回 `True`。
---
[05:00](https://www.youtube.com/watch?v=QxOdRkXpQwE&t=300s) 自定义数据结构的真值协议与无限递归规避
许多人看到 CPython 源码中的 `if-else` 结构时,会直觉地以为自定义对象如果不是内置类型,就会直接掉入底部的 `else` 分支。然而高天指出事实并非如此:底部的 `else` 仅在对象的类型既没有 `tp_as_number`,也没有 `tp_as_mapping` 和 `tp_as_sequence` 时才会触发,这种情况只有在 C 语言扩展中自定义类型时才可能出现。对于在 Python 代码中用 `class` 语法定义的任何自定义类,它们的 `tp_as_number` 等结构指针并不是 `NULL`。
在 Python 层面定义的类会统一走入特定的槽位(slot)分配机制。类型上的 `nb_bool` 槽位调用了一个专门的处理函数,该函数严格按照以下顺序执行两件事: 1. **优先查找 `__bool__` 方法**:若类中定义了双下划线 `__bool__` 方法,则直接调用该方法。 2. **次选查找 `__len__` 方法**:若没有定义 `__bool__`,则退而求其次寻找是否定义了双下划线 `__len__` 方法。 3. **默认回退机制**:若既没有定义 `__bool__` 也没有定义 `__len__`,该函数直接返回 `1`。
> “也就是说如果你的自定义数据结构既没有定义这个 bool,也没有定义这个 length,那他在这个 if statement 里相当于是 if True。”
取得这些魔术方法的返回值后,CPython 内部会再次调用 `PyObject_IsTrue` 对其进行评估。此时可能会引发一个疑问:外层通过 `PyObject_IsTrue` 调入该函数,内层又再次调用 `PyObject_IsTrue`,是否存在死循环或无限递归(infinite recursion)的风险?
答案是绝对不会。CPython 源码在两处做了强类型校验以杜绝无限递归: - 如果调用的是 `__len__`,源码会强制检查返回值是否为合法的非负整数,若不是可以表示长度的类型会立即抛出异常;合法的长度数值在下一步便能直接判定为真或假。 - 如果调用的是 `__bool__`,CPython 源码在第 6585 行显式检查其返回值类型是否为精确的 `bool`(`PyBool_Check`),若不是布尔类型,在第 6589 行会直接抛出 `TypeError` 报错。
得到返回值后,底层代码做了一个规整化操作:若返回值大于 0 则直接返回 1;对于非正数则进行降采样类型转换(downcast),保留 0 或负数错误码。在日常工程编码中,高天强调: > “我其实还是更推荐大家去做更明确的判断,其中一个就是去保证 type,你可以把 A 这个值的 type 范围压缩的越小越好。”
因为 `if a:` 对于 `None`、`False`、`0` 以及空列表 `[]` 均判定为假,而在复杂的业务系统中,遇到空列表与遇到 `None` 往往需要完全不同的应对逻辑。因此只有在变量类型非常收敛、明确的情况下才适合简写为 `if a:`。
---
[10:00](https://www.youtube.com/watch?v=QxOdRkXpQwE&t=600s) `bool` 的本质与 `bool(a)` 在条件判断中的冗余
关于第四种写法 `if bool(a):`,很多人直觉上认为 `bool` 是一个返回布尔值的普通内置函数。然而从 CPython 源码来看,`bool` 根本不是函数,而是一个内置的类(class)。在 `bltinmodule.c` 中,`bool` 对应的其实是 `PyBool_Type`。
查看 `boolobject.c` 中该类型的 `tp_new` 构造函数,在第 52 行可以看到,它在构造新的布尔实例时,底层调用的依然是老朋友 `PyObject_IsTrue`: - `tp_new` 调用 `PyObject_IsTrue` 计算入参的真假性。 - 随后直接调用 `PyBool_FromLong`,将结果包装并返回全局单例 `Py_True` 或 `Py_False`。
正因为 `bool(a)` 表面上像一个函数,但其实际构造过程完全依托于 `PyObject_IsTrue`,所以代码中的 `if bool(a):` 与单纯的 `if a:` 在真值判断逻辑上是完全等价的。
两者的唯一区别在于运行效率:在 `if` 语句中显式套上一层 `bool(a)`,只是额外增加了一次函数/类型的调用开销(function call),完全属于多此一举的操作。当然,在需要显式将一个任意数据显式转换为标准的 `True` 或 `False` 单例并进行传递、存储时,`bool(a)` 依然有其存在的价值;但若仅仅放在 `if` 关键字后作条件分支,则没有任何额外作用。最后,码农高天在视频末尾提醒观众如果喜欢内容记得点击关注,不要忘记“一键三连”。
## Notable quotes > [00:00](https://www.youtube.com/watch?v=QxOdRkXpQwE&t=0s) 在我们写程序的时候,脑子里面一定要想着,这个变量它有可能是 True 和 False 之外的东西。 — 码农高天
## Mentioned Python, CPython, 码农高天, PyObject_IsTrue, POP_JUMP_IF_FALSE, PyBool_Type, PyBool_Check, PyBool_FromLong, boolobject.c, bltinmodule.c
## Video description 这期视频聊一下if里判断的规则。在python这种灵活的语言里,一个任意类型的变量是怎么被当成bool值进行真假判断的呢?
[Source](https://www.youtube.com/watch?v=QxOdRkXpQwE)