码农高天
代码中选择assert还是raise应取决于语义而非运行优化模式,程序逻辑正确时不应发生的情况须用assert断言,外部不可控的运行时故障方适用raise。
AI-assisted summary of 【Code Review】raise还是assert?降低重复代码?给视障人士的支持! — original source. Check the original for full context.
Source published:
Key ideas
- 码农高天对一个为视障屏幕阅读器 NVDA 切换国内镜像源的开源插件进行了 Code Review,并联动 B 站投币活动向 NVDA 官方组织发起公益捐款。 (0:00)
- 在防御性编程与错误处理中,高天澄清了 `assert` 与 `raise` 的选择标准在于语义而非工具开关:`assert` 应用于代码正常时不应发生的内部不变量破坏,而 `raise` 用于即使逻辑正确也可能出现的外部不可控异常。 (5:00)
- 针对插件核心函数 `swap_url` 和退出清理逻辑中的重复代码,高天提出了提取公共执行逻辑与利用参数化开关合并函数等重构方案。 (7:20)
Detailed notes
## Background 该视频由 YouTube 频道“码农高天”于 2024 年 9 月 4 日发布。视频内容源自高天对其知识星球用户编写的 NVDA 镜像源切换插件进行的 Code Review,旨在通过具体代码探讨重构技巧,并呼吁公众关注与支持服务于视障群体的开源屏幕阅读软件 NVDA。
## TL;DR 高天以一个 NVDA 镜像切换插件为例,深入分析了 Python 项目中的代码规范与重构思路。他首先梳理了命名风格冲突时的权衡策略,以及如何合理界定模块级常量的范围。随后针对关键的 URL 替换函数,他探讨了分支缺失的问题,并从语义学角度厘清了 `raise` 与 `assert` 的根本差异——`assert` 用于声明不应发生的不变量,而 `raise` 用于处理即便代码无误也可能出现的外部不可控异常。最后,高天演示了如何提取分支中的公共重构逻辑,并通过增加可选参数将高度雷同的缓存清理操作合并复用。
## Atmosphere & style 视频整体呈现出一种沉稳专业且兼具人文关怀的技术探讨氛围。高天延续了其一贯从容、循循善诱的讲解风格,没有生硬地套用教条,而是从实际工程体验出发剖析语法与结构取舍。伴随着对视障群体与开源精神的呼吁,技术审查不仅停留在代码技巧层面,更带有一种务实且温和的公益温度。
## Bulletin - [00:00](https://www.youtube.com/watch?v=vbF5M1L2SnU&t=0s) 码农高天对一个为视障屏幕阅读器 NVDA 切换国内镜像源的开源插件进行了 Code Review,并联动 B 站投币活动向 NVDA 官方组织发起公益捐款。 - [05:00](https://www.youtube.com/watch?v=vbF5M1L2SnU&t=300s) 在防御性编程与错误处理中,高天澄清了 `assert` 与 `raise` 的选择标准在于语义而非工具开关:`assert` 应用于代码正常时不应发生的内部不变量破坏,而 `raise` 用于即使逻辑正确也可能出现的外部不可控异常。 - [07:20](https://www.youtube.com/watch?v=vbF5M1L2SnU&t=440s) 针对插件核心函数 `swap_url` 和退出清理逻辑中的重复代码,高天提出了提取公共执行逻辑与利用参数化开关合并函数等重构方案。
## Key takeaways - [00:00](https://www.youtube.com/watch?v=vbF5M1L2SnU&t=0s) NVDA 是一款开源免费的视障屏幕阅读器软件,完全依赖社区捐款支持开发运作。 - [01:15](https://www.youtube.com/watch?v=vbF5M1L2SnU&t=75s) 在参与既有开源项目时,若原有代码库自身的命名风格不统一,开发者无需过度纠结,依赖 IDE 自动补全即可。 - [02:00](https://www.youtube.com/watch?v=vbF5M1L2SnU&t=120s) 在模块级别仅被赋值一次的量,即使是通过计算得出,也应作为常量并遵循全大写加下划线的命名规范。 - [02:30](https://www.youtube.com/watch?v=vbF5M1L2SnU&t=150s) 核心功能函数如 `swap_url` 的命名应当明确具体职责并封装进插件类中,同时明确声明返回值的作用。 - [03:30](https://www.youtube.com/watch?v=vbF5M1L2SnU&t=210s) 在版本判断逻辑中缺失 `else` 分支会导致隐式返回 `None`,应改为显式处理或提前校验返回,消除歧义。 - [05:00](https://www.youtube.com/watch?v=vbF5M1L2SnU&t=300s) 选择 `assert` 还是 `raise` 的核心判断依据是语义,而非 Python 的 `-O` 优化跳过机制。 - [05:20](https://www.youtube.com/watch?v=vbF5M1L2SnU&t=320s) `assert` 适用于假设程序编写正确情况下绝不应该触发的内部断言,用于保证内部逻辑一致性。 - [05:40](https://www.youtube.com/watch?v=vbF5M1L2SnU&t=340s) `raise` 适用于即使代码完全无误,也会因外部输入或无法控制的外部环境而不可避免发生的异常。 - [06:40](https://www.youtube.com/watch?v=vbF5M1L2SnU&t=400s) 在多版本分支判断中,应尽量只在分支内配置差异化的变量与模块,将后续共同操作提取到分支外部统一执行。 - [07:20](https://www.youtube.com/watch?v=vbF5M1L2SnU&t=440s) 文件与缓存清理逻辑应直接复用已知路径变量,避免通过文件路径反射查找父目录的多余操作。 - [07:40](https://www.youtube.com/watch?v=vbF5M1L2SnU&t=460s) 针对主体逻辑完全一致、仅存在个别额外校验步骤的清理函数,应通过引入配置参数进行合并抽象以消除代码冗余。
## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=vbF5M1L2SnU&t=0s) NVDA 插件背景与命名风格
本期 Code Review 的代码来自知识星球的一位用户,他编写了一个用于 NVDA 的小插件来切换国内镜像源。码农高天首先向观众普及了 NVDA:它代表 Non-Visual Desktop Access(非视觉桌面访问),并不是显卡厂商英伟达,而是一款完全开源且免费的屏幕阅读软件,能通过语音帮助视障人士了解屏幕内容。由于支撑该软件开发的唯一经济来源是社区捐助(donation),高天发起了一项公益联动:在视频发布一周内,B 站视频每获得 100 个投币,他就会向 NVDA 组织捐赠 1 澳元(约合 5 元人民币,即每个投币 1 澳分),以此呼吁大家关注视障人士这一特殊群体。
```mermaid flowchart TD accTitle: NVDA Plugin Lifecycle accDescr: Global plugin initialization swapping URLs and termination reverting them. subgraph InitStage ["Plugin Init (init)"] A["Save original URL constant"] --> B["Replace with mirror URL"] B --> C{"API Version Check"} C -->|"Version Meets Requirement"| D["Call swap URL function"] end subgraph TerminateStage ["Plugin Terminate (terminate)"] E["Restore original URLs"] --> F["Delete Cache Directories"] end InitStage -.->|"On NVDA Exit / Reload"| TerminateStage ```
进入代码分析,插件的核心任务是替换 NVDA 主程序中的部分下载网址,使国内用户可以顺畅使用镜像源下载。阅读代码开头时,高天指出了一些值得探讨的代码风格问题: - **特殊的第三方模块与命名风格**:导入的几个模块在本地常规 Python 环境中并不存在,且采用了 Python 中较少见的 `lowerCamelCase`(小驼峰)命名,这属于 NVDA 原生代码的历史包袱。 - **变量风格不一致的应对**:模块下方声明常量与版本号时,又切换回了小写下划线风格。当面临“已有项目的代码风格与自身习惯冲突”时,高天认为通常应优先遵循现有代码库风格;但如果原有代码自身风格就不统一,开发者便“想怎么写怎么写,不用太纠结”,毕竟现代 IDE 的补全提示已经非常完善。 - **常量的定义范畴**:“对于这种整个 module 只赋值一次的量,哪怕它是计算得出的,你也可以把它当做常量看待。”高天强调,只要赋值后不再改变,即便依赖其他数据计算而来(例如当前 API 版本号或 JSON 文件路径),也应当遵循常量的规范使用全大写字母加下划线命名。
---
[02:30](https://www.youtube.com/watch?v=vbF5M1L2SnU&t=150s) 函数职责与 `swap_url` 的防御性逻辑
插件的本体结构遵循了 NVDA 的规范,通过继承 `globalPlugin` 类,在初始化(`init`)时执行 URL 替换,在退出前(`terminate`)将原本的 URL 恢复并清理缓存。但在具体实现细节上,代码存在不少模糊地带:
原插件定义了一个全局函数 `swap_url`,用来替换不同 API 版本下的网络 Base URL,同时重新初始化 Data Manager 并清理缓存。高天指出: 1. **命名过于宽泛**:初次阅读时很难一眼看出函数具体替换了什么、返回了什么。建议将其命名为更具针对性的名字(如表明是替换 network base URL),并建议直接作为方法封装进 `globalPlugin` 中,附带注释说明会返回“被替换前的旧 URL”。 2. **缺失的 else 分支**:在 `swap_url` 内部,代码仅写了 `if` 和 `elif` 对应不同的版本号判断,完全没有 `else` 分支。虽然外层调用前做过前置版本校验,使得进入函数时必然满足某一分支,但高天直言:“就你看着它,你并不知道如果这个 Current API Version 太小了应该发生什么事。他现在的结果是 return None 对吧,这个事不是不能接受,但是如果你真要这么写的话,你最好把它写的显式化。”
若要改善这种模糊性,有两种处理方向:一种是若版本过低就显式提前 `return None`,调用方省去外层校验直接通过返回值是否为 `None` 来处理;另一种是将其定位为一个强假设函数——版本不足时压根不准调用该函数。
---
[05:00](https://www.youtube.com/watch?v=vbF5M1L2SnU&t=300s) `raise` 与 `assert` 的语义之争及代码去重
针对前述函数前置条件未满足的情况,代码应选用 `assert` 还是 `raise`,往往在社区中引发激烈争论。有人主张 Python 在开启优化选项(`-O` 模式运行)时会跳过 `assert` 检查,因此不应使用 `assert`。高天纠正了这一误区,指出工具开关并不是决定语法的根本标准:
“一个地方用 assert 还是用 raise,核心考量是语义。如果这个地方在我程序编写正确的情况下压根就不应该出现,那用的就是 assert,他传达的语义是这件事不应该发生。但如果这个地方的情况是我程序就算写的没有问题,它依然有可能因为种种原因发生,同时这个事我还解决不了,这块就应该用 raise。”
为了说明两者的差异,高天举例说明: - **应使用 `raise` 的场景**:比如除法中除数为零,这并非由程序自身逻辑 Bug 引起,而是由无法预料的外部用户输入所致,程序在正常运行下无法计算,此时必须 `raise` 异常。 - **应使用 `assert` 的场景**:函数内部对内部调用的前置条件做防御,在调用栈闭环、代码逻辑正确时不应该被触发的分支,使用 `assert` 声明内部不变量最为切合语义。
```mermaid flowchart TD accTitle: Refactoring Redundant Code in swap_url accDescr: Moving common post-replacement logic outside the API version branch. subgraph BeforeRefactor ["Before: Redundant Code"] direction TB B1{"API Version Branch"} B1 -->|"Branch A"| BA1["Set Mod A Base URL"] --> BA2["Re-init Data Manager"] --> BA3["Clear Cache & Return URL"] B1 -->|"Branch B"| BB1["Set Mod B Base URL"] --> BB2["Re-init Data Manager"] --> BB3["Clear Cache & Return URL"] end subgraph AfterRefactor ["After: Extracted Common Logic"] direction TB A1{"Early Validation"} -->|Version Check| A2{"Select Target Module"} A2 -->|"Branch A"| AM1["Module A Config"] A2 -->|"Branch B"| AM2["Module B Config"] AM1 --> AC["Unified Execution: Re-init Data Manager + Clear Cache + Return Old URL"] AM2 --> AC end ```
除了语义判断,`swap_url` 中还存在明显的重复代码:在 `if` 和 `elif` 两个分支中,只有开头提取和替换 URL 的具体模块名和属性有细微差别,后续重新初始化 Data Manager、清理缓存以及返回旧 URL 的操作完全一致。高天建议通过重构提炼公共操作,在分支内部只做区分性的变量赋值,随后统一执行公共逻辑。这样既消除了冗余,又能在开头对非法版本做“早期返回(early return)”或异常断言,让代码的执行主线清晰易读。
---
[07:20](https://www.youtube.com/watch?v=vbF5M1L2SnU&t=440s) 缓存删除逻辑的复用与抽象
在插件清理缓存的代码实现中,高天指出了两处可以进一步优化和重构的设计:
| 关注代码段 | 现存设计问题 | 优化方案与建议 | |:--- |:--- |:--- | | `delete_addon_store_cache` | 通过把文件名与路径拼接成 JSON 完整路径,读取后再通过路径反射找其父文件夹并删除 | 直接复用上文已经拼装好的存储目录变量,避免反复从文件反推父目录,逻辑更清晰直接 | | `terminate` vs `delete_addon_store_cache` | 退出函数中清理缓存的逻辑与专门的删除缓存函数高度雷同,仅多了一道检查条件 | 增加参数(如 `check_mirror=True`)统一为一个函数,独立抽离“检查镜像是否匹配”的逻辑 |
高天对比了 `delete_addon_store_cache` 函数与 `terminate` 阶段的缓存清理代码,发现两者做的事情几乎完全一致:定位 JSON 缓存、读取内容、按条件删除目录。唯一的差异在于是否需要额外校验当前的 URL 是否确实为镜像源地址。
针对这种高度相似的代码,高天给出的重构方案是避免复制代码,而是在原有函数中增加一个默认形参(例如 `check_mirror=True`)。如果不传参,则默认先检查镜像地址是否存在于该 JSON 中;如果明确不需要检查,则直接执行删除操作。通过把“检查 Mirror URL 是否存在”抽象为一个辅助判定或可配置开关,两个函数的主要删除逻辑便可以合并通用。
视频最后,高天再次呼吁大家多关注视障人士以及视障程序员这个相对特殊且需要支持的群体,并提醒观众视频投币将会全额转化成捐款支持 NVDA 项目的长期开源开发。
## Notable quotes > [00:00](https://www.youtube.com/watch?v=vbF5M1L2SnU&t=0s) 对于这种整个 module 只赋值一次的量,哪怕它是计算得出的,你也可以把它当做常量看待。 — 码农高天
## Mentioned NVDA, 码农高天, Python, B站, 知识星球, NV Access
## Video description Code Review是提升代码水平最快的方式之一,这期视频我们review一个NVDA的插件。代码比较简单,但是也有不少可以讨论的地方。更重要的是,希望大家了解视障人士这个群体。(投币捐款的活动是在B站做的,已经结束啦,大家就看code review就好)
本次代码:https://github.com/hwf1324/NVDAUpdateMirror/blob/feature-API-Update/addon%2FglobalPlugins%2FupdateMirror.py 支持NVDA:https://www.nvaccess.org/support-us/
[Source](https://www.youtube.com/watch?v=vbF5M1L2SnU)