高天拆解crab_dbg漏洞 指手写解析与静态导库拖垮调试工具

【Code Review】做了个我做过的东西?这不撞枪口上了么! — 码农高天

码农高天

crab_dbg因顶层导入重型库引发数秒延迟、手写字符串解析破坏合法语法并缺乏循环引用防护,远未达到可用的产品级标准。

AI-assisted summary of 【Code Review】做了个我做过的东西?这不撞枪口上了么! — original source. Check the original for full context.

Source published:

Key ideas

  • 高天评测了复刻 Rust dbg! 宏的 Python 库 crab_dbg,指出用跨语言概念做核心卖点会严重缩窄受众,且忽视了已有的 IceCream 和 object-print 等成熟竞品。 (0:00)
  • 该库在模块顶层静态尝试导入 PyTorch 等重量级依赖,若环境存在 torch 会导致任何基础打印操作都平白增加数秒启动开销。 (3:22)
  • 函数通过栈帧读取源码并手写字符串状态机切分参数,在 REPL 交互环境、多行注释字符串、带引号的逗号或括号场景下极易直接崩溃。 (5:00)
  • 对象递归展开模块未引入 visited 机制检测循环引用,遇到自引用列表或双向链表时会直接触发 RecursionError 爆栈。 (12:00)

Detailed notes

## Background 该视频由 YouTube 频道“码农高天”于 2025 年 4 月 5 日发布。视频内容是对一个名为 crab_dbg 的轻量级开源 Python 库进行 Code Review,高天结合自己曾经开发维护过同类功能库(object-print)的经历,从产品定位与工程实现等多个维度对该项目展开剖析。

## TL;DR 高天首先批评了该库将功能生硬锚定为“Rust dbg! 宏的 Python 替代品”的产品定位,指出这既缩小了潜在受众,也忽略了 Python 生态中如 IceCream 和 object-print 等成熟竞品。在代码工程层面,视频深入揭露了其顶层直接导入 PyTorch 等重量级库导致的数秒启动延迟、基于源码切片与手写括号栈解析实参表达式时在 REPL、引号、注释及别名场景下的频繁崩溃。此外,视频还剖析了其对象格式化模块缺乏针对多行缩进的处理和循环引用防护等致命缺陷,强调编写高级调试工具时应优先保障鲁棒性与优雅降级,而非让用户进程原地崩溃。

## Atmosphere & style 整期视频充满着技术老手的敏锐、犀利与幽默,“撞枪口上”的坦率开场奠定了降维打击式的专业底色。高天一边带着“我当年也踩过这些坑”的过来人体谅,一边毫不留情地当场构建边缘用例连续击穿作者的天真假设,让人看得既忍俊不禁又深感震撼。整体节奏从宏观的产品设计层层递进至底层的 AST、栈帧与递归引用,极具观赏性和硬核教学启发感。

## Bulletin - [00:00](https://www.youtube.com/watch?v=WfUY2-jtjlA&t=0s) 高天评测了复刻 Rust dbg! 宏的 Python 库 crab_dbg,指出用跨语言概念做核心卖点会严重缩窄受众,且忽视了已有的 IceCream 和 object-print 等成熟竞品。 - [03:22](https://www.youtube.com/watch?v=WfUY2-jtjlA&t=202s) 该库在模块顶层静态尝试导入 PyTorch 等重量级依赖,若环境存在 torch 会导致任何基础打印操作都平白增加数秒启动开销。 - [05:00](https://www.youtube.com/watch?v=WfUY2-jtjlA&t=300s) 函数通过栈帧读取源码并手写字符串状态机切分参数,在 REPL 交互环境、多行注释字符串、带引号的逗号或括号场景下极易直接崩溃。 - [12:00](https://www.youtube.com/watch?v=WfUY2-jtjlA&t=720s) 对象递归展开模块未引入 visited 机制检测循环引用,遇到自引用列表或双向链表时会直接触发 RecursionError 爆栈。

## Key takeaways - [00:00](https://www.youtube.com/watch?v=WfUY2-jtjlA&t=0s) 以‘用 A 语言实现 B 语言功能’为卖点是产品定位硬伤,会将目标用户生硬缩窄为同时熟悉两门语言的极小交集。 - [00:00](https://www.youtube.com/watch?v=WfUY2-jtjlA&t=0s) 带文件名、行号等元信息的调试打印在 Python 社区已有成熟轮子(如 IceCream 和 object-print),开发新库必须先做竞品调研找准差异化壁垒。 - [00:00](https://www.youtube.com/watch?v=WfUY2-jtjlA&t=0s) 直接照搬 Rust dbg! 的排版导致数十字符的绝对路径挤占前置视线,破坏了控制台阅读的信息密度与视线路径。 - [03:22](https://www.youtube.com/watch?v=WfUY2-jtjlA&t=202s) 轻量级 utility 库严禁在模块顶层无节制地静态 import 大型科学计算库(如 torch),否则会引入不可接受的数秒延迟。 - [05:00](https://www.youtube.com/watch?v=WfUY2-jtjlA&t=300s) 依赖抓取 caller frame 重新读源文件提取表达式的黑魔法无法在 REPL 或交互式环境中工作,脱离实体文件会直接报错。 - [05:00](https://www.youtube.com/watch?v=WfUY2-jtjlA&t=300s) 使用 strip 和硬编码切片 [4:-1] 剥离函数名的做法极其脆弱,无法应对带空格调用 debug (1) 或起别名赋值 p = dbg 等合法语法。 - [09:59](https://www.youtube.com/watch?v=WfUY2-jtjlA&t=599s) 在缺乏 AST 语法树或正规词法分析支持的情况下,纯手写字符扫描解析器无法正确处理字符串内部包裹的逗号与括号。 - [09:59](https://www.youtube.com/watch?v=WfUY2-jtjlA&t=599s) 手写 parser 搭配粗暴的 assert 断言会导致解包传参(*args)等合法 Python 语法直接让程序异常崩溃。 - [09:59](https://www.youtube.com/watch?v=WfUY2-jtjlA&t=599s) 高级且易错的黑魔法功能应当设计为可选降级,在解析失败时退化输出值本身远优于直接中断用户进程。 - [12:00](https://www.youtube.com/watch?v=WfUY2-jtjlA&t=720s) 递归展开复杂对象必须记录已访问对象内存 ID,缺乏循环引用防护会导致自引用数据结构引发 RecursionError 无限递归死锁。

## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=WfUY2-jtjlA&t=0s) 产品定位的偏差与竞品分析

本期 Code Review 评测的是一个完整但小巧的 Python 库——`crab_dbg`。作者对其功能定位的描述非常简单:“Python equivalent to Rust dbg! macro”,即在 Python 中实现 Rust 语言里的 `dbg!` 调试宏功能。以往的视频大多聚焦在代码质量本身,而这次高天选择首先从产品设计的维度来剖析该项目。

高天尖锐地指出,这个项目的基本锚点存在逻辑缺陷: > “首先用一个语言去完成另一个语言已经写过的东西这件事是写 Rust 那群人的专利,除了那群人,其他人一般不这么折腾。”

如果一个库宣称核心卖点是用 A 语言复刻 B 语言的功能,其直接后果就是潜在用户群体被生硬地压缩成了“既懂 A 语言又懂 B 语言”的极小交集。如果一个开发者从未用过 Rust、不知道 `dbg!` 宏是什么,他就很难对这个 Python 库产生任何兴趣。

从本质来看,Rust 的 `dbg!` 宏实际上就是一个功能更丰富的 `print`。它不仅能输出变量的值,还会额外附带代码所在的文件名、行号,甚至支持自动展开用户自定义的类。例如定义一个变量 `pi = 3.14`,调用后不仅能显示其数值,还会注明在哪个文件第几行打印了名为 `pi` 的变量。但这种附加元信息的打印机制并非 Rust 独占。在着手写一个库或产品前,做竞品调研至关重要。在 Python 生态里早已有同类成熟产品: - **IceCream**:知名度极高,提供了一个谐音梗函数 `ic()`。执行 `ic(123)` 时会自动打印被执行的表达式与值;直接调用一个无参的 `ic()` 即可直接输出当前执行的文件与行号。不过它缺乏对用户自定义类结构的深度展开能力。 - **object-print**(高天曾开发维护的项目):不仅具备行号、文件名与表达式打印能力,还能以极具可读性的格式层层展开打印复杂的自定义类对象。

高天总结了产品设计上的两点警示: 1. 完全没有必要将这种普适的“带元信息的变量打印”需求死板地与 Rust 概念强行绑定。 2. 前人早已做过完善的轮子,如果要打造一个真正具备竞争力的产品而非自娱自乐的玩具,就必须挖掘独特的创新点与壁垒。

随后高天从 UI/排版观感上指出该项目的一大硬伤:信息密度与视线路径设计糟糕。该库直接照搬了 Rust `dbg!` 的输出布局,前面塞了整整 60 多个字符的绝对文件路径和行号信息,而真正关键的变量名与数值被挤到了最后。每打一行都引发严重的换行与视线剧烈左右摆动。Rust 的官方例子之所以观感舒适,是因为其示例输出的是相对路径(字符非常短);这也是为什么 `object-print` 当初在设计输出时,刻意只抓取文件名而不展示完整绝对路径,且在排版换行上进行了精密的人性化考量。

---

[03:22](https://www.youtube.com/watch?v=WfUY2-jtjlA&t=202s) 代码结构与沉重的第三方库导入陷阱

进入源码细节后,该库只有一个名为 `_debug.py` 的单文件。高天提到文件名没必要带下划线前缀,顶部的 `import` 与 `from... import` 按照规范也应该分块书写,但这属于次要编码风格问题。代码中最致命的工程硬伤在于它处理第三方库支持的方式:

```mermaid flowchart TD accTitle: 第三方库导入流程隐患 accDescr: crab_dbg 在工具库初始化阶段静态顶层尝试导入 torch 等重型库,引发不必要的启动延迟。

subgraph Problem ["crab_dbg 当前实现"] direction TB A["加载 crab_dbg 模块"] --> B["顶层 try...except 导入 numpy, pandas, torch"] B --> C{"当前环境是否存在 torch?"} C -- "是" --> D["耗时 2~3 秒执行 import torch"] C -- "否" --> E["忽略异常"] D --> F["任何极小的 print 都要白白承受数秒延迟"] end ```

代码试图兼容 NumPy、Pandas 和 PyTorch(torch)等常用科学计算库,若环境存在就 import 进来,不存在就忽略 pass。然而,对于一个主打轻量敏捷的基础 utility 库,在顶层直接静态尝试导入重型库是不可接受的: > “别的不说,咱就说这个 torch,在一般的电脑上 import 一个 torch 可能要花两三秒的时间。换句话说,如果我当前的环境有 torch,我只要用到你这个库,就要白花两三秒的时间来 import torch。对于我来说这个问题就已经给这个库判死刑了。”

用户无法接受一个仅仅充当 `print` 替代品的微型调试工具会让脚本多出整整两三秒的执行开销。高天结合自己开发开源库的经历谈到,如何优雅地处理 Lazy Import(惰性导入)是极具挑战的深水区。如果仅仅把 `import torch` 塞进下层函数里,根本解决不了“如何在不导入 torch 的前提下判断当前对象是否为 `torch.Tensor`”的循环论证问题。

---

[05:00](https://www.youtube.com/watch?v=WfUY2-jtjlA&t=300s) 获取调用帧与源码提取的黑魔法及崩塌场景

该函数在设计承诺上号称是 `print` 的“perfect replacement”(完美替代品)。高天直言,既然立下了这个誓言,就必须面对各种真实复杂场景的考验。

为了拿到调用该函数时的源码表达式,函数利用黑魔法抓取了 `caller frame`(调用栈帧)。因为在 Python 传参机制中,函数内部只能感知到实参的具体值(如拿到数值 `1`),而无法天然获知调用方写的是变量名 `a` 还是字面量。作者通过栈帧中的文件名重新打开并读取源代码文件。

这一设计在以下场景中当场崩溃: - **交互式命令行(REPL / Interactive Shell)**:在交互式环境中直接执行调用,由于调用帧并没有对应的实体源文件存在,函数直接抛出异常崩溃。

即使在能找到源文件的场景下,该项目获取参数原始表达式的函数(`get_debug_raw_args`)同样漏洞百出。它利用 Python `inspect` 库提供的行号区间截取函数调用的行,对每行执行 `strip` 放入列表中,随后的代码逻辑展现了多重典型错误:

1. **遍历列表的同时执行 In-place 修改**:代码在 `enumerate` 循环原始列表的同时进行原地修改。在 Python 中应该尽量避免在迭代时原地变异容器,而应在内部生成新列表,或者直接串联 Generator 处理。 2. **多行函数调用的行内注释删除漏洞**:作者的意图是将多行展开的调用合并回一行,并清理行内注释。其逻辑是判断行内是否含有 `#`,若有则调用 `tokenize` 解析出非注释部分重新拼接。但在复杂语法下该逻辑彻底失效: - 如果用户打印的是一个多行长字符串(Multi-line String),其中某一行正好以 `#` 开头,该算法会武断地将整个包含业务内容的文本行当成 Python 源码注释抹杀掉。 - 对多行字符串粗暴执行 `strip` 破坏了原本的多行对齐与格式。 - 若在多行字符串参数内部增加一个 `#`,会导致参数解析数量对不上号,程序当场抛出异常爆炸。

高天演示,该代码合并参数为一行后,使用死板的切片 `[4:-1]` 试图剔除开头的 `dbg(` 和末尾的 `)`。这种天真的字符串切片完全禁不起推敲: - 如果调用时写成带空格的 `debug (1)`,切片后只剩半个括号,导致输出变成残缺的 `(1 = 1`。 - 如果用户通过赋值起别名:`p = dbg; p(1)`,代码直接炸裂。 上述各种写法全部属于完全合法的 Python 语法,标准内置 `print` 皆能从容应对。

---

[09:59](https://www.youtube.com/watch?v=WfUY2-jtjlA&t=599s) 手写字符解析(Parsing)的灾难与鲁棒性设计

提取出参数主体文本后,作者实现了一个名为 `split_by_outermost_comma` 的函数,旨在依据最外层逗号切分出每个独立实参,同时保留内层表达式自带的逗号。

作者为此手写了一套状态机机制:逐字扫描字符,遇到左括号进栈,遇到对应的右括号出栈;当且仅当遇到逗号且栈为空时,判定为最外层逗号并进行参数拆分。高天对此直言不讳: > “当我们看到这种自己纯手写的基于字符串的 parsing 的时候,我们脑海里就有四个字对吧,肯定不行。”

由于该算法完全没有针对字符串字面量(引号内的字符)建立分词状态识别,任何将特殊字符包裹在引号内的合法传参都会摧毁这个栈: - 打印一个单独的逗号字符串 `dbg(",")`:直接崩溃。 - 打印一对括号字符 `dbg("(", ")")`:逻辑混乱报错。 - 打印任何包含小括号、大括号或逗号的复杂文本:全线崩溃。

手写基于字符的粗糙 parser 根本不可能正确解析语法元素,此类底层解析必须借助于 Python 官方健全的 AST(抽象语法树)或成熟的词法解析机制。随后代码中粗暴的 `assert` 断言(断言解析出的表达式个数必须等于运行时传入的值数量)成为了灾难放大器。在遇到解包传参(如 `dbg(*args)`)时,表达式与参数数量永远无法匹配,断言失败导致程序彻底罢工。

在此高天分享了一条核心的产品级开发准则: > “对于这种高级的、难度比较大的、你知道有可能会出错的功能,尽量给它做成可选的……输出一个‘我不知道’,比你原地爆炸是要好非常多的。”

在极端边界情况下,即使无法推导还原表达式名字,退化输出值本身(如 `object-print` 的做法)也远好于直接崩死用户进程。

---

[12:00](https://www.youtube.com/watch?v=WfUY2-jtjlA&t=720s) 对象格式化、递归展开与循环引用死锁

流程进入最末端的对象可视化打印模块(即怎么把任意 Python 对象转换成人性化的可读文本):

- **变量命名规范**:作者在内部命名大量采用了前置/后置下划线来规避与系统内置变量重名。高天建议这种风格极不 Pythonic,直接更换一个表意更明确的变量名即可。 - **缩进排版缺陷**:在遍历展开 `list` 或 `tuple` 时,作者使用递归并在其返回串最前端强加缩进。由于它完全依赖内层调用的缩进,导致遇到内部自定义了多行格式的对象时,只有多行的第一行加上了缩进,后续行全盘错位。相比之下,`object-print` 会自适应维护整块多行文本的基准缩进层次。 - **容器覆盖不全**:作为一个通用打印库,最起码应覆盖 Python 全套内置容器(至少要包含 `set`),代码目前只覆盖了列表、元组和字典,覆盖率过窄。 - **缺乏循环引用防护(致命缺陷)**:这是整个对象展开逻辑中最严重的系统设计漏洞。作者完全依赖朴素的递归进行对象图遍历,没有任何针对访问路径的 `visited` 集合追踪:

```mermaid flowchart LR accTitle: 递归遭遇循环引用 accDescr: 朴素递归打印自身引用的列表导致 RecursionError 无限递归死锁。

A["List 对象 A"] --> B["A.append(A)"] B --> C["递归格式化 A 的元素"] C --> A style A fill:#f9f,stroke:#333,stroke-width:2px ```

当对象图内存在循环引用(例如一个列表通过 `a.append(a)` 将自己包含在内,或者在实现双向链表 `Double Linked List` 时前驱后继节点相互指引),该库毫无防备,直接陷入无限递归直到触发 Python 最大递归深度(`RecursionError`)彻底崩溃。无论是 Python 自带的容器 `print` 还是成熟的 `object-print`,都会记录访问过的对象内存 ID 并安全打出 `<Recursion on...>`。

高天总结道,因为自己曾经亲手从零做过完全同类性质的产品,所以深知这套链路上的每一处暗坑与细节考量。目前来看,`crab_dbg` 作为个人的练手玩具尚可,但若想真正包装成一款在开源界推广立足的产品,无论是在产品定义、代码鲁棒性还是算法结构上,均是任重而道远。

## Notable quotes > [09:59](https://www.youtube.com/watch?v=WfUY2-jtjlA&t=599s) 对于这种高级的、难度比较大的、你知道有可能会出错的功能,尽量给它做成可选的……输出一个‘我不知道’,比你原地爆炸是要好非常多的。 — 码农高天

## Mentioned 码农高天, crab_dbg, Rust, dbg!, IceCream, object-print, Python, NumPy, Pandas, PyTorch

## Video description 这次我们review一个小巧但是完整的库。这个库实现了一个我曾经做过的功能,所以很难得地我们可以从产品的角度去聊聊这个代码。

本次代码:https://github.com/WenqingZong/crab_dbg

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

中文简报

高天拆解crab_dbg漏洞 指手写解析与静态导库拖垮调试工具 crab_dbg因顶层导入重型库引发数秒延迟、手写字符串解析破坏合法语法并缺乏循环引用防护,远未达到可用的产品级标准。 高天首先批评了该库将功能生硬锚定为“Rust dbg! 宏的 Python 替代品”的产品定位,指出这既缩小了潜在受众,也忽略了 Python 生态中如 IceCream 和 object-print 等成熟竞品。在代码工程层面,视频深入揭露了其顶层直接导入 PyTorch 等重量级库导致的数秒启动延迟、基于源码切片与手写括号栈解析实参表达式时在 REPL、引号、注释及别名场景下的频繁崩溃。此外,视频还剖析了其对象格式化模块缺乏针对多行缩进的处理和循环引用防护等致命缺陷,强调编写高级调试工具时应优先保障鲁棒性与优雅降级,而非让用户进程原地崩溃。 高天评测了复刻 Rust dbg! 宏的 Python 库 crab_dbg,指出用跨语言概念做核心卖点会严重缩窄受众,且忽视了已有的 IceCream 和 object-print 等成熟竞品。 (0:00) 该库在模块顶层静态尝试导入 PyTorch 等重量级依赖,若环境存在 torch 会导致任何基础打印操作都平白增加数秒启动开销。 (3:22) 函数通过栈帧读取源码并手写字符串状态机切分参数,在 REPL 交互环境、多行注释字符串、带引号的逗号或括号场景下极易直接崩溃。 (5:00) 对象递归展开模块未引入 visited 机制检测循环引用,遇到自引用列表或双向链表时会直接触发 RecursionError 爆栈。 (12:00)

Reading summary

码农高天对 Python 调试库 crab_dbg 进行了深度评测,指出该项目在产品定位、启动开销、源码解析与对象格式化上存在多处硬伤,与成熟替代品相比仍有较大差距。 产品定位狭窄与竞品壁垒不足 (0:00) crab_dbg 将自身定位为 Rust 语言中 dbg! 宏的 Python 复刻版本。高天认为,这种以复刻另一门语言为卖点的做法,生硬地将潜在用户限制在“既懂 Python 又懂 Rust”的极小交集内,而未能聚焦“带元信息的变量打印”这一通用诉求。此外,Python 生态中已有知名度极高的 IceCream(提供 ic() 函数打印表达式与行号)以及高天此前开发的 object-print(支持深度展开自定义类),crab_dbg 缺乏独特的技术壁垒。 在输出布局上,该库照搬了 Rust 的格式,在关键变量前堆砌了 60 多个字符的绝对路径和行号信息,导致单行严重溢出与视线剧烈左右摆动。高天指出,Rust 官方用例观感良好是因为其示例路径较短,而在 Python 工具设计中,理应像 object-print 一样仅抓取简短文件名,并进行人性化的排版与换行优化。 顶层导入重型库引发启动延迟 (3:22) 在代码结构方面,crab_dbg 仅包含一个 _debug.py 文件。该库在顶层通过 try...except 尝试静态导入 NumPy、Pandas 和 PyTorch(torch)等常用科学计算库,若环境存在即载入,若不存在则忽略。 高天指出,对于一个定位为替代 print 的基础轻量工具,在模块加载时静态导入重型库是不可接受的工程缺陷。以 torch 为例,在常规设备上导入一次需要 2 至 3 秒,这意味着宿主环境中只要安装了 torch,任何微小的打印调用都会白白承受数秒的启动延迟。高天强调,通用工具库若要优雅支持第三方数据结构,必须解决深水区的惰性导入(Lazy Import)问题,而不是在顶层进行粗暴的尝试性加载。 源码抓取逻辑漏洞与合法语法失效 (5:00) crab_dbg 号称是 print 的“完美替代品”,并通过获取调用栈帧(caller frame)读取源文件的方式来反推调用处的参数表达式。然而,这一机制在缺乏物理源码文件的交互式命令行(REPL / Interactive Shell)中会直接崩溃。 在参数提取函数 get_debug_raw_args 中,代码暴露出多项逻辑缺陷:在 enumerate 循环列表的同时进行原地修改;遇到以 # 开头的多行字符串内容时,会武断地将其误删为 Python 注释;粗暴的 strip 操作破坏了原本的多行格式。此外,代码采用固定切片 [4:-1] 剔除外层调用括号,当用户采用 debug (1) 等带有空格的写法,或者使用赋值别名(如 p = dbg; p(1))时,解析结果就会产生残缺甚至直接抛出异常。 手写字符解析缺陷与鲁棒性缺失 (9:59) 为了在获取参数源码后依据最外层逗号切分实参,作者手写了一套利用栈处理括号配对的状态机。高天直言这种基于纯字符的手写 parser 根本无法保证正确性。由于该状态机未对字符串字面量建立分词识别,一旦参数中包含带引号的逗号或括号(如 dbg(",") 或 dbg("(", ")")),解析栈便会全线崩溃。 在处理这类底层语法元素时,代码应借助官方的抽象语法树(AST)或成熟的词法解析机制。同时,代码中粗暴使用 assert 断言解析出的表达式数量必须等于运行时实参数量,导致在遇到解包传参(如 dbg(*args))时程序必然因断言失败而崩溃。高天提出,对于难度较高、易出错的功能,应将其设计为可选降级模式,遇到无法解析的边界情况输出未知提示,远好于直接崩死用户进程。 对象展开排版错位与循环引用死锁 (12:00) 在末端的可视化打印模块中,代码不仅在命名上大量使用不 Pythonic 的前后置下划线,且内置容器覆盖面较窄,连基础的 set 都没有涵盖。在多行排版时,由于展开逻辑完全依赖内层缩进,导致自定义多行对象在打印时只有首行具有缩进,后续文本完全错位。 更为致命的是,该库在递归展开对象图时,完全没有设置记录已访问对象内存 ID 的 visited 机制。当面对自身引用的列表(如 a.append(a))或相互引用的双向链表等循环引用场景时,代码会陷入无限递归,直到触发 Python 的最大递归深度(RecursionError)彻底崩溃。高天指出,成熟的打印工具都会追踪访问路径,并在检测到重复对象时安全输出递归标记。 高天总结认为,crab_dbg 作为个人练手项目尚可,但若想作为严肃的开源库推广立足,其在产品定义、启动性能、代码鲁棒性以及复杂对象处理等诸多维度上均任重而道远。

Source quotes

[9:59] 码农高天: 对于这种高级的、难度比较大的、你知道有可能会出错的功能,尽量给它做成可选的……输出一个‘我不知道’,比你原地爆炸是要好非常多的。

Original transcript

[0:00] 又是大家期待已久的code review节目啊这期code review 呢要感谢字节跳动和trade的大力支持我们这次review的是一个完整但是小巧的酷叫crabdbg它的说明非常简单啊叫python equivalent to rustdbg macro就是它在python里实现了 rust的这个debug的红那我们以往的视频呢基本上都是在review代码本身而这次的review会从产品的角度去表达一下我自己的观点比如说我觉得这个项目的锚点本身是有问题的首先用一个语言去完成另一个语言已经写过的东西这件事是写Rust那群人的专利除了那群人,其他人一般不这么折腾。你很少会见到有人用其他的语言去完成RATS的功能,对吧?其次如果你的项目的核心概念是用A语言完成了B语言的功能那么你的潜在用户群体就一下变成了A语言和B语言的交集如果我没用过B语言不知道这个功能我就不可能会对你的酷产生兴趣那我们看他做的到底是一个什么东西呢他这个所谓debug macro实际上就是一个fancy的print他本质上就是在打印变量只不过除了打印之外啊他还有一些额外的功能比如说他会打印这个文件名和行数包括他可以展开这个用户自定义的类他打印出来的东西效果大概是这样比如这里啊pi=3.14然后你在下面啊打印这个pi他这就会告诉你在哪个文件在哪一行打印了一个叫pi的变量这个pi是3.14那大家会发现这个功能它其实并不是什么Rust专属的东西它不就是一个附加了额外信息的打印功能吗我经常会说在你做一个库啊或者说一个产品之前要先做竞品调研市面上其实有很多拍摊的库是做这件事情的呀最有名的应该是这个啊Ice Cream他提供了一个函数IC啊谐音感IC我们看他在打印ic-123的时候,下面就是可以显示被打印的expression的当然这个值肯定也被打印对吧它的格式有些不同那一个裸的IC就会打印你当前在哪个文件的哪一行当然Ice Cream这个酷是没有提供用户自定义class的展开功能的那另一个功能上更完善的酷叫做object print我们可以看到前面自定义的类用object print打印出来就是下面这个效果同时object print也支持打印当前的行数和文件名以及打印你给的expression本身我们回过头来也就是说第一你完全没有必要把这个功能和Rust绑定这本身是一个非常常见的需求第二你做这些东西呢前辈们都已经做过了如果你真的想去弄一个产品而不是玩具你就要继续去找你的创新点和你更有优势的地方那么回到这个打印的格式本身大家刚才看了一些其他的格式回过头来看这个格式的第一感觉是什么对于我来说就是前面这个部分太长了我们人眼对于一行内的信息处理能力是有限的它前面这个文件和行数的部分有60多个字符而真正对于用户来说非常重要的信息在特别后面的地方你看他下面这个换行就会让你的视线左右摆动极大这个格式本身应该是直接照搬的Rust的但是我们注意Rust这里用的是相对路径它是比我们看到的绝对路径要短不少的所以Rust这个例子的观感是明显要比Target的例子要好的这也是为什么Object Print只选了文件名本身而没有拿全部的路径同时还做了换行的原因好的这个是我对产品本身的一些观点啊我们接下来看代码它代码结构非常简单其实就一个文件我觉得这个文件没有必要命名成下滑线debug.py但是这个没有那么重要前面import的部分我曾经说过啊import跟fromimport应该分开下面这个部分是他尝试对第三方库座的支持对于这三个库如果能找到就import进来如果找不到就不管但是这里大家有没有意识到一件事就是你作为一个本来应该很小巧的 utility 库你竟然在尝试import一些很大的库别的不说咱就说这个touch在一般的电脑上import一个touch可能要花两三秒的时间换句话说如果我当前的环境有touch我只要用到你这个库就要白花两三秒的时间来import torch对于我来说这个问题就已经给这个库判死刑了你能想象你的python代码只要用到print这个script至少就要跑两秒吗不可能的嘛我在做QuartumPi的时候花了非常长时间去解决这个lazy import的问题就怎么能在需要Torch的时候才import这个库这件事情没有那么简单他才判断一个value是不是一个TorchTensor的时候是要用到Torch的也就是说不是你把import torch放到一个函数里这个事就解决了这里面有个循环论证的问题你怎么在不impose 拖势的情况下判断 [5:00] 虚无需要用到Torch好 我们直接拉下来看它入口函数这里说了啊这个函数的实现是要做一个print的perfect replacement那你既然承诺了这件事你就要经得起考验对吧所有我能用到print的地方我把print换成你这个函数他应该都能用下面他先拿到了color frame他要从这个ColorFrame里面拿到额外的信息比如说我这个函数是在哪个文件哪一行调用的还有就是我调用的时候传进来的这个expression是什么大家要意识到这件事没有那么简单在python中我们传参的时候传的是一个变量的值在这个函数里面我们只知道传进来的这个值是1-值给hackx了但是我们想要打出来A等于1不是一件容易的事情这个函数内部是没有这个信息的这个算一个小小的黑魔法啊我们来看它怎么实现的首先通过这个frame的file name来打开这个原代码好 请问没有原代码怎么办我们看在interactive shell里面我们做一个简单的debug1直接它就崩溃了你并不总能找到你的color frame的原代码那如果你宣称自己是一个print的perfect replacement你就不能在这个时候崩溃对吧好 假设他找到原代码了接下来他通过get debug raw args来拿到这些原始的expression他这里也注释了拿到所有的参数首先把source code按行分开然后拿到debug call的这几行他这个positions是python inspect这个库提供的他会告诉你这个函数的调用是从哪行到哪行然后他把这些行都strip一下变成一个list好 这里开始出现问题了他下面做的第一件事是先删掉所有的inline comments但是我们注意他在做enumeration的同时在修改这个正在被循环的变量这个是我们在python中要尽量避免的事情尽管他这个代码本身啊很有可能是对的但是你不知道这里面可能有什么坑所以我们尽量不要在循环的时候做这种in place的修改我们应该在循环内部去建立一个新的list如果我们建立新的list的话我们就没有必要把我们原来这个原始数据给变成list了大概是这样啊原来这个原始数据可以就是一个 generator 然后我们 loop 这个 generator再把我们需要的数据给拼起来最后把拼起来的这些行变成一行它整个这一段代码的目的是把一个可能分布到很多行的函数调用给拼回一行同时删掉所有的注释但是我们看他的逻辑对吗他删掉注释的方法是在这一行里面找这个警号如果这一行里包含警号就通过Tokenite这个库把这一行给解析成Token然后把不是注释的部分拼起来看起来没什么问题对吧比如我们简单的跑一个例子debug hello 都号world这里它正确的识别了我打印的第一个expression是hello我打印的第二个expression是world这个注释没有影响到任何的行为那如果我打印的是这么个东西呢我是一个Multi-line string其中有一行是警号 PEST这个时候你就会发现他没有办法识别到这个expression了他把我的第六行整行都当做注释了如果没有警号应该是这样的当然他把这个multi line string给strip这件事看起来也是有问题的对吧因为它上面这个等式怎么看怎么变那如果我们打印的东西长成这个样子呢?一个Multi-Line String Test到号1他应该能识别出两个 argument 一个test 一个1 对吧这里我们认为他没有问题我们把test前面加一个警号 改变一下这个string好 直接爆炸很容易理解对吧他把整个这一行就当做注释了然后参数的数量对不上号了你是没有办法只通过这一行的信息来判断这个东西是不是注视的好我们回到他代码啊他把整个这个调用变成一行接下来删掉前面的debug括号和最后一个括号只留中间的argument他的删法是4冒号-1也就是说如果我的调用是debug空格括号1的话不出意外的话结果就是半辣括号1等于1如果我做一个P=dbg P1的话 直接把他弄炸了弄炸了我们注意一下啊这些方法都是合法的python使用方法这些方法print都是可以完成的那同样的例子我们未给object print看一下可以看到object print没有处理多行调用的这个inline comment这个结果是错误的但是其他几个例子都是没有问题的好那拿到了这整个argument string之后他做了一个split by outmost comma也就是他试图把这个string啊用逗号分隔开但是分隔开的时候他要保留这个expression内部的逗号 most comma我们看这个部分是他的做法他这个做法就是一个字符的筛过去如果 [10:00] 叫左括号就压债遇到右括号并且看到站里是对应的左括号就出站遇到逗号并且站里没有东西他就认为是最外层的逗号这种情况下他就认为这是一个part当我们看到这种自己纯手写的基于字符串的parsing的时候我们脑海里就有4个字对吧肯定不行我们来上几个例子吧我们来尝试打印一个逗号直接爆炸不好使我们来尝试打印一个左括号和一个右括号不好使打印一个左括号和1呢保持基本上只要这个argument里面包含了一个string并且这个stream里面有一些特殊字符他这一套系统就不好使因为他根本就没有判断这个大括号小括号逗号是不是stream的一部分它整个这个parse的路子就是不对的你不可能自己手写这个东西你这个部分一定是利用python已经有的结构的最后把这些argument的whitespace清除掉然后反过回来在这里他就拿到了所谓的RAW ARX也就是我们前面说的这些参数的expression本身而不是他们的值这个assertion大家刚才已经见过很多次了它parse出来这些expression跟实际传进来的expression数量不一致的情况时有发生比如这个例子对吧我们这个arx是1 2 3然后我们把它在函数调用的时候解包那object print在这里给出结果显然也是错的对吧但是他这个酷就直接炸了那这里就是我们做产品的另外一个经验啊对于这种高级的难度比较大的你知道有可能会出错的功能尽量给他做成可选的我们看左边同样的对print的直接替代object print是可以打出东西来的但是这位同学的酷直接就爆炸了这种原地爆炸是会让你的用户非常沮丧的事情就比如我们刚才说的如果你拿不到source code你可能不知道这个argument是什么但是你输出一个我不知道比你原地爆炸是要好非常多的好那拿到了这些argument的名字和这些argument的值之后就是按照某一个格式打印了他最后这个部分呢其实是object print做的核心内容怎么把任意的一个python object打印成人类可图的形式首先这个部分我没有特别喜欢这种下挖线的写法啊他这里是为了避免和内置变量的变量没有冲突但是我觉得你换个名字就行了对吧这种Anderscore的形式看起来不是很排桑啊这里面如果这个object是一个list或者是一个tuple他就做一个地规啊然后把前面加一个锁进大家注意一下他这个缩近是只加在了他这个地规结果之前的也就是说他完全依赖他的地规结果把后面的缩近高明白如果我后面这个结果本身打印出来是有格式的呢比如我们这里有个personly他自己定义了一个自己简单的格式而这个格式是多行的我们现在有两个人构成了一个list我们看上面这个是object print打印的结果他是把所有的内容都做了一个缩紧而下面这个是他这个酷答应的结果你就会看到只有第一行做了缩紧在格式上我认为这样是不那么理想的接下来他处理了字典的情况然后检查是不是知识的第三方数据以及这个东西有没有自己定义好的REPR或者street那这里有件事啊就是一般来说我们在做酷的时候要覆盖built-in containers最少最少你应该也要把set给覆盖了只管三个container可能稍微点少后面如果是一个一点都不特殊的python object他就去查这个object的dict这个地方跟前面的处理形式基本是一样的这里大家意识到一件事没有他是用地龟来完成这个事情的而我们的术语结构里面是很有可能产生循环引用的我们举一个最简单的例子A是一个list然后我们让Aappend了自己我们可以看到print跟object print都是可以处理这种情况的然而他这个库肯定就无限递归了对吧在 target的立子里面有包括了 linked list我不知道他有没有尝试一下double linked list因为一个双向链表就会产生这种无限低规的问题在他这个代码里他没有进行处理所以在实际的应用中会产生很多问题我们这次的code review比之前都要长一些一个很重要的原因是这个酷正好涉及到了我擅长的领域就他这个东西啊我做过所以我知道哪块有可能会出问题所以这是一个很难得的机会我们可以不光去看代码还可以看这个产品本身总的来说我觉得这个酷的大方向是可做的但是目前来看他现在的状态并没有比那些已经存在的库多出来一些创新和突破作为一个玩具来说还不错但是作为一个真的你想去推的产品来说还是任重而造远好 这期视频这里啊 喜欢的话 点赞关注 约了一件三连哦