码农高天
统计计数器合并时必须按累计分子除以分母而非直接累加频率,且业务代码应将静态配置抽离为字典驱动以彻底杜绝exec动态赋值。
AI-assisted summary of 【Code Review】这么长一串相似代码怎么处理?另外数学不对也是错误哦! — original source. Check the original for full context.
Source published:
Key ideas
- Code Review 强调数据与逻辑分离原则:将构造函数中大段重复初始化的静态配置提取为独立数据文件,用外部配置驱动两行循环完成实例化,可大幅提高代码可维护性。 (0:00)
- 避免使用类似 `self.w`、`self.s` 的散乱对象属性,建议用统一的字典结构(如 `self.structures`)集中管理实例,防止污染类命名空间并简化键名映射。 (0:00)
- 审查指出加法运算符重载(`__add__`)中存在严重的数学逻辑错误:合并两个计数器时不能直接相加频率,因为分子与分母同时变化,衍生比率指标必须重新计算。 (5:00)
- 严禁在合并属性或常规业务赋值中滥用 `exec()` 动态执行代码;动态属性访问应优先使用 `getattr`/`setattr`,重构为字典后则直接使用标准字典操作。 (5:00)
Detailed notes
## Background 本期视频由博主“码农高天”于2024年2月13日发布在YouTube平台。视频属于其Code Review系列,主要审查开源句法复杂度分析库neosca的核心数据结构模块,分享如何重构大段相似代码并纠正代码中的逻辑与数学计算错误。
## TL;DR 高天深入审查了neosca项目中负责句法结构计数的核心文件,指出其在初始化时硬编码大量静态常量导致的冗长与维护难题,建议通过“数据与逻辑分离”将配置抽离并用字典统一管理。随后,他针对代码中变量类型中途变更、元数据与统计数据混存等设计细节提出了优化建议。最后,高天指出了加法运算符重载中动用exec()动态赋值的反模式,并纠正了直接将两组频率简单相加的严重数学逻辑错误。
## Atmosphere & style 视频整体呈现出专业、坦诚且条理分明的技术评审氛围。高天在肯定作者良好排版习惯的同时直切要害,语气既有资深工程师的严谨犀利,又不乏循循善诱的启发感;当指出用exec()拼接代码和频率直接相加的离谱错误时,其直截了当的吐槽既接地气又具戏剧性,让观众在观摩代码重构的同时深切体会到规范工程实践与严谨数学逻辑的重要性。
## Bulletin - [00:00](https://www.youtube.com/watch?v=TKVJONxA_OM&t=0s) Code Review 强调数据与逻辑分离原则:将构造函数中大段重复初始化的静态配置提取为独立数据文件,用外部配置驱动两行循环完成实例化,可大幅提高代码可维护性。 - [00:00](https://www.youtube.com/watch?v=TKVJONxA_OM&t=0s) 避免使用类似 `self.w`、`self.s` 的散乱对象属性,建议用统一的字典结构(如 `self.structures`)集中管理实例,防止污染类命名空间并简化键名映射。 - [05:00](https://www.youtube.com/watch?v=TKVJONxA_OM&t=300s) 审查指出加法运算符重载(`__add__`)中存在严重的数学逻辑错误:合并两个计数器时不能直接相加频率,因为分子与分母同时变化,衍生比率指标必须重新计算。 - [05:00](https://www.youtube.com/watch?v=TKVJONxA_OM&t=300s) 严禁在合并属性或常规业务赋值中滥用 `exec()` 动态执行代码;动态属性访问应优先使用 `getattr`/`setattr`,重构为字典后则直接使用标准字典操作。
## Key takeaways - [00:00](https://www.youtube.com/watch?v=TKVJONxA_OM&t=0s) 使用 `None` 作为函数的默认参数并在函数体内判定后再实例化 `set`,是避免 Python 可变对象默认参数共享陷阱的标准做法。 - [00:00](https://www.youtube.com/watch?v=TKVJONxA_OM&t=0s) 类初始化时充斥大段建立静态实例的重复代码,应将配置数据剥离到独立模块,以实现数据与业务逻辑的解耦。 - [00:00](https://www.youtube.com/watch?v=TKVJONxA_OM&t=0s) 使用形如 `self.w`、`self.s` 的离散属性如同用 `A_0`、`A_1` 代替数组,既污染对象命名空间又损害代码语义,应改用单一字典集中管理。 - [00:00](https://www.youtube.com/watch?v=TKVJONxA_OM&t=0s) 在方法签名中添加裸星号(`*`)强制特定参数为关键字实参(Keyword-only arguments),可有效防止调用时传参顺序错位。 - [05:00](https://www.youtube.com/watch?v=TKVJONxA_OM&t=300s) 保持局部变量生命周期内的类型一致性,避免在同一变量中从 `tuple` 动态转换为 `list`。 - [05:00](https://www.youtube.com/watch?v=TKVJONxA_OM&t=300s) 在集合筛选和解析场景中,采用内置的 `filter` 函数相比手写循环在表达语义上更加清晰直接。 - [05:00](https://www.youtube.com/watch?v=TKVJONxA_OM&t=300s) 若底层采用字典按键管理结构,导出字段时可直接支持包含斜杠等字符的键名,无需在后续处理中做字符串替换。 - [05:00](https://www.youtube.com/watch?v=TKVJONxA_OM&t=300s) API 返回的数据结构应进行清晰的分层设计,将文件名等元数据(`meta`)与统计频次(`freqs`)分开,避免同级平铺混存。 - [05:00](https://www.youtube.com/watch?v=TKVJONxA_OM&t=300s) 在对象相加时直接对频率求和存在数学错误,合并衍生比率指标必须遵循分子累加除以分母累加的计算逻辑。 - [05:00](https://www.youtube.com/watch?v=TKVJONxA_OM&t=300s) 普通业务代码中应当坚决避免使用 `exec()` 这类黑魔法,可用内置反射函数或改用容器字典优雅解决属性合并问题。
## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=TKVJONxA_OM&t=0s) 代码结构与数据逻辑分离重构
在这一期 Code Review 中,码农高天审查了项目 `neosca`(一个用于句法复杂度分析的工具库)。尽管该项目的 README 制作精美附有动图,但 GitHub Actions 的构建处于失败状态。由于整个仓库体积较大,本期只专注于其中的核心数据结构文件 `structureCounter`。该模块包含两个核心类:`Structure` 用于保存单个句法结构的数据(如结构名称、描述、匹配模式 `pattern` 以及依赖的外部句法结构 `requirements`);`StructureCounter` 则是核心计数器,负责在接收输入文件后统计单词、句子、动词等多种句法结构的出现情况,并提供 `get_freqs` 等方法来导出词频。
在进入构造函数 `__init__` 后,输入参数包含 `selected_measures`(用于指定要统计的数据类型)。作者在此处的处理非常规范:当传入参数为 `None` 时新创建一个 `set`,避免了使用可变对象作为默认参数的常见陷阱。然而在接下来的初始化过程中,代码中充斥着大段大段建立 `Structure` 实例的重复逻辑。原作者虽然在排版上做得很工整,运用了合理换行和长字符串折叠技巧,但代码依然显得冗长杂乱。每一个 `Structure` 对象的属性名直接映射为结构的名称,同时绑定了若干固定的配置属性,这些本质上全部是静态常量。高天指出,这种场景最适合进行“数据与逻辑分离”: > “你要改数据就去找数据,要改逻辑就来找逻辑,会让代码可维护性变高一些。”
为了从根本上精简代码并提升扩展性,高天建议将所有构建 `Structure` 所需的常量属性抽取至独立的文件(例如配置数据模块),在 `__init__` 中仅通过两行代码完成循环导入与实例化。这样一来,日后如果要调整类名或重构逻辑,只需修改一处。此外,原代码将每一个结构实例挂载在 `self.w`、`self.s` 等独立的类属性上,这种做法等同于用 `A_0 = 0`、`A_1 = 1` 来代替数组,既污染了对象的命名空间,语义也不够明确。更好的做法是将所有结构集中维护在一个单一的字典 `self.structures` 中,直接使用结构名称作为键。对于构造参数中出现的关键字参数 `requirements`,为了避免调用者传参错位到 `pattern` 上,可以通过在方法签名中添加裸星号(`*`)来强制其必须以关键字形式传递(Keyword-only argument)。
```mermaid flowchart TD accTitle: 数据与逻辑分离重构流程 accDescr: 展示将硬编码类属性转换为外部常量数据并用字典驱动初始化的重构对比
subgraph Before["重构前:逻辑与数据耦合"] A1["__init__ 内部硬编码大量常量"] --> B1["动态绑定单个属性:self.w, self.s..."] B1 --> C1["代码冗长且命名空间被污染"] end
subgraph After["重构后:数据驱动初始化"] A2["独立配置文件 / 常量字典"] -->|import 引入| B2["循环遍历配置集合"] B2 --> C2["统一存入字典:self.structures[name]"] end ```
- 原仓库通过 `Structure` 对象保存元信息:结构名、描述、匹配模式(`pattern`)及前置依赖结构(`requirements`)。 - 默认参数陷阱防范:使用 `None` 作为默认值,在函数体内部判定为 `None` 时再实例化空 `set`,避免了共享可变默认实参。 - 命名空间与语义优化:避免使用 `self.w`、`self.s` 这类散乱属性,改用字典结构 `self.structures = {}` 统一按名索引。 - 强制关键字传参(Keyword-only arguments):在形参列表插入 `*`,确保 `requirements` 等重要配置项必须通过明确的 key 传入,防止位置参数错位。
---
[05:00](https://www.youtube.com/watch?v=TKVJONxA_OM&t=300s) 类型一致性、字段转换与加法运算符重载中的数学错误
在完成基础结构的建立后,代码定义了候选集合变量 `structures_to_query`。原作者起初将其初始化为一个 `tuple`,随后为了配合后续的过滤操作又将其转化为 `list`,导致该变量在运行期间经历类型变化(标注为 `Sequence[Structure]`)。高天建议在能避免的情况下尽量不要让同一个变量在生命周期内存放多种类型,直接自始至终使用 `list` 即可,这本身也是一种微型的数据逻辑分离。同样的问题也出现在 `structures_to_report` 上。紧接着在解析用户传入的筛选度量时(`parse_selected_measures`),原代码手写循环对候选集合进行筛选,实际上使用内置的 `filter` 函数在语义上会更加清晰直接。
在导出字段与运算方面,代码暴露出若干设计上的别扭与潜在缺陷: 1. **字段名替换与字典格式**:原代码处理导出的字段时通过字符串替换将下划线(`_`)强行转换为斜杠(`/`)。如果此前按照建议使用字典管理结构,键名本身就可以直接包含斜杠,省去繁琐的字符串变换。 2. **元数据与指标数据混存**:在返回词频字典的方法中,代码将普通元数据(如 `file_name`)与句法结构计数结果直接混用同级 key 存放在同一个字典内。更规范的做法是对字典做层级拆分,例如外层分为 `meta`(存放文件名等元数据)和独立的 `freqs` 或 `structures` 字段,以提升数据接口的整洁度。 3. **加法操作符(`__add__`)中的数学逻辑错误**:类中重载了相除(`__truediv__`)和相加(`__add__`)操作。在合并两个 `StructureCounter` 实例时,代码直接将计算出的频率值相加。高天对此提出了尖锐的批评: > “频率的计算是分子除以分母的关系,当你把两个 counter 合并到一起的时候,是不是分子变了,分母也变了?那么新的频率怎么可能是两个原有频率的相加呢?所以我感觉在数学上这块就错了。” 4. **反模式代码滥用**:在加法合并属性时,原实现居然动用了 `exec()` 动态执行代码。高天强调,`exec()` 属于能避免就必须避免的黑魔法;即便不改用字典,也完全可以用内置的 `setattr()` 与 `getattr()` 替代;而一旦将底层重构成字典驱动,整个合并流程只需普通字典更新即可优雅解决。
- 变量类型稳定性:避免让变量在同一函数内从 `tuple` 动态变成 `list`,保持单一变量类型的纯粹性。 - 数据结构分层:API 返回的结果字典应分离业务元数据(如文件名)与统计指标(各结构频次),避免平铺导致 key 污染。 - 数学运算的严谨性:加权比率、发生频率等由“分子/分母”构成的衍生指标,合并统计时绝不可直接求和,必须重新用分子累加值除以分母累加值。 - 动态属性操作的安全性:严禁在简单业务赋值中使用 `exec()`,属性反射应使用 `getattr`/`setattr`,容器化重构则直接采用字典操作。
## Notable quotes > [00:00](https://www.youtube.com/watch?v=TKVJONxA_OM&t=0s) 你要改数据就去找数据,要改逻辑就来找逻辑,会让代码可维护性变高一些。 — 码农高天
> [05:00](https://www.youtube.com/watch?v=TKVJONxA_OM&t=300s) 频率的计算是分子除以分母的关系,当你把两个 counter 合并到一起的时候,是不是分子变了,分母也变了?那么新的频率怎么可能是两个原有频率的相加呢? — 码农高天
## Mentioned 码农高天, neosca, GitHub Actions, structureCounter, Python
## Video description 其他Code Review:https://www.youtube.com/playlist?list=PLSo-C2L8kdSNr5yUJYhyDArnM4FU9iG1S
Code Review是提升代码水平最快的方式之一,这期视频的主角是处理句法结构数据的data structure。
本次repo:https://github.com/tanloong/neosca/
[Source](https://www.youtube.com/watch?v=TKVJONxA_OM)