码农高天指单行循环超时设计存硬伤 栈帧反射拖垮Python性能

【Code Review】while loop超时检查?不可能实现的设计? — 码农高天

码农高天

执念于单行 while 语法会陷入 XY 问题,通过多例模式和栈帧反射检查超时不仅无法中断循环体内部阻塞,还会因 Python 3.11 栈帧转换带来严重性能损耗,超时控制应改用 with 上下文管理器。

AI-assisted summary of 【Code Review】while loop超时检查?不可能实现的设计? — original source. Check the original for full context.

Source published:

Key ideas

  • 码农高天通过对开源项目 timeplus 的 Code Review,深入剖析了试图用单行 while max_time(3) 实现循环超时控制背后的严重设计缺陷与逻辑漏洞。 (0:00)
  • 为在单行内保存循环开始时间,作者引入了基于线程号、调用栈深度及代码行号的多例模式,但极易在函数复用或嵌套循环等基础场景中状态错乱。 (5:00)
  • 自 Python 3.11 起 frame 已下沉至 C 语言层面,循环中高频反射调用栈会导致巨大的性能损耗,出现超时机制反客为主拖慢业务的严重问题。 (10:00)
  • 该案例是典型的 XY 问题,说明当顶层架构设计走偏时,后续的代码黑魔法和实现细节根本无法弥补设计本身的先天不足。 (10:00)

Detailed notes

## Background 该视频由 YouTube 频道“码农高天”于 2024 年 10 月 20 日发布。视频内容是一期代码审查(Code Review),针对一位初学者在开源项目 timeplus 中尝试用极简方式实现循环超时检查的设计展开深入剖析与讨论。

## TL;DR 高天审查了一个试图用单行 `while max_time(3):` 语法实现超时中断的开源项目,指出其在循环体阻塞时完全无法生效、无法兼容 `for` 循环等根本缺陷。为了强行达成单行调用的表象,原作者引入了复杂且脆弱的多例模式(Multiton),依赖检查调用栈来拼凑状态标识,不仅在重复调用与嵌套循环中极易崩溃,还在 Python 3.11+ 下带来了极高的 C 栈转换性能损耗。高天总结指出,这是典型的软件开发“XY 问题”——因执着于错误的技术路径而偏离了合理的架构设计,限制代码生命周期更应遵循 Python 直觉,采用 `with` 上下文管理器等正统方案。

## Atmosphere & style 视频风格务实、犀利而又不失包容,带有一种资深工程师在工位旁悉心点拨后辈的沉浸感。高天在指出遗留死代码和“屎山雏形”时言辞直击要害、妙趣横生,但同时肯定了初学者规范的命名习惯与探索热情。整场讲解层层剥茧,从表层语法一路深挖到底层 C-level frame 机制与软件设计哲学,让观众在会心一笑的同时获得深刻的认知升级。

## Bulletin - [00:00](https://www.youtube.com/watch?v=Jf3IttpFrkw&t=0s) 码农高天通过对开源项目 timeplus 的 Code Review,深入剖析了试图用单行 while max_time(3) 实现循环超时控制背后的严重设计缺陷与逻辑漏洞。 - [05:00](https://www.youtube.com/watch?v=Jf3IttpFrkw&t=300s) 为在单行内保存循环开始时间,作者引入了基于线程号、调用栈深度及代码行号的多例模式,但极易在函数复用或嵌套循环等基础场景中状态错乱。 - [10:00](https://www.youtube.com/watch?v=Jf3IttpFrkw&t=600s) 自 Python 3.11 起 frame 已下沉至 C 语言层面,循环中高频反射调用栈会导致巨大的性能损耗,出现超时机制反客为主拖慢业务的严重问题。 - [10:00](https://www.youtube.com/watch?v=Jf3IttpFrkw&t=600s) 该案例是典型的 XY 问题,说明当顶层架构设计走偏时,后续的代码黑魔法和实现细节根本无法弥补设计本身的先天不足。

## Key takeaways - [00:00](https://www.youtube.com/watch?v=Jf3IttpFrkw&t=0s) 将本地缓存目录 __pycache__ 提交至 GitHub 仓库,暴露出初学者未正确配置 .gitignore 的常见工程规范问题。 - [00:00](https://www.youtube.com/watch?v=Jf3IttpFrkw&t=0s) 在 while 条件求值阶段判断超时无法打断正在执行的循环体,若循环内部发生耗时阻塞(如 time.sleep(10)),程序依旧会完整等待。 - [00:00](https://www.youtube.com/watch?v=Jf3IttpFrkw&t=0s) 强行将超时检查绑定在 while 条件中,导致设计无法自然兼容常规的 for 循环,限制了 API 的通用性。 - [00:00](https://www.youtube.com/watch?v=Jf3IttpFrkw&t=0s) 函数调用应当尽量保持纯粹,而单行 while max_time(3) 必须依赖隐式状态来记忆初始时间,违背了纯函数的设计原则。 - [05:00](https://www.youtube.com/watch?v=Jf3IttpFrkw&t=300s) 为强行省去显式声明 Timer 对象的代码行,作者不得不引入多例模式(Multiton Pattern)来区分不同循环的实例状态。 - [05:00](https://www.youtube.com/watch?v=Jf3IttpFrkw&t=300s) Python 运行时在 __new__ 返回已有实例后仍会强制执行 __init__,作者为此添加防御属性,但更优雅的方案是直接将初始化逻辑留在 __new__ 中。 - [05:00](https://www.youtube.com/watch?v=Jf3IttpFrkw&t=300s) 遗留的废弃参数和未使用的 __next__ 方法反映出改动方案后未及时清理旧代码,正是日常“代码屎山”形成的原型。 - [10:00](https://www.youtube.com/watch?v=Jf3IttpFrkw&t=600s) 依赖线程 ID、栈深度、文件名和行号拼凑的四元组标识极其脆弱,同一函数被连续调用两次或外层嵌套循环时均会导致状态复用失效。 - [10:00](https://www.youtube.com/watch?v=Jf3IttpFrkw&t=600s) Python 3.11 将栈帧下沉至 C-level,每次循环检查调用栈都会强制解释器生成 Python-level frame,造成不可忽视的性能开销。 - [10:00](https://www.youtube.com/watch?v=Jf3IttpFrkw&t=600s) 该项目深陷 XY 问题:作者为了追求单行写法的局部需求 Y,耗费精力设计复杂实现,却忽视了原本目标 X 的更合理路径。 - [10:00](https://www.youtube.com/watch?v=Jf3IttpFrkw&t=600s) 在 Python 中控制一段逻辑的生命周期与超时打断,最地道且符合直觉的语法载体是基于 with 的上下文管理器(Context Manager)。

## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=Jf3IttpFrkw&t=0s) 项目初识与 `while max_time` 设计的逻辑缺陷

码农高天在本期 Code Review 中分析了一位知识星球大学生的开源项目(`timeplus`)。从 GitHub 仓库结构来看,代码略显青涩,最明显的问题是把本应被 `.gitignore` 忽略的本地缓存目录 `__pycache__` 提交了上去;同时,整个仓库只包含一个类的实现文件 `limited_time.py` 和一个使用示例,这种极简结构根本不需要在根目录下放 `__init__.py`。

抛开工程文件结构,该项目最核心的功能是试图通过一个 `max_time` 函数来限制一个循环的最大执行时间。作者构想的直观语义是写成 `while max_time(3):`,意图让这个循环最多执行 3 秒,若超时则抛出自定义的运行时超时异常(如 `TimeoutError`)。它的基本实现思路并非采用 signal、multiprocessing 或 multithreading 等复杂并发机制,而是在每次循环调用 `max_time(3)` 时取当前时间与记录的开始时间作差比对,未超时返回 `True`,超时则报错。

```mermaid flowchart TD accTitle: while max_time 执行流程 accDescr: 展示在 while 循环中每次条件求值时调用 max_time 检查超时的运行与漏洞机制

subgraph Evaluation ["循环条件求值阶段"] A["进入 while 条件判断"] --> B["调用 max_time(3)"] B --> C{"当前时间 - 开始时间 > 3s?"} C -- "未超时" --> D["返回 True,执行循环体"] C -- "超时" --> E["抛出超时异常 / 终止"] end

subgraph LoopBody ["循环体执行阶段"] D --> F["执行业务逻辑代码"] F --> G["循环单次耗时过长 (例如 sleep 10s)"] G --> H["超时完全无法被中断!"] H --> A end ```

这种语法设计引入了显而易见的逻辑漏洞: - **循环体内超时无法被中断**:超时判断仅仅发生在 `while` 条件判别的那一瞬间。如果循环体内部执行了耗时操作(例如 `time.sleep(10)`),程序依然会完整睡眠 10 秒,在循环内部发生阻塞时该设计毫无应对办法。 - **无法兼容 `for` 循环**:由于超时检查与 `while` 表达式强行绑定,开发者无法对常规的 `for` 循环进行限制,必须把所有的 `for` 循环手动改写为等价的 `while` 循环才能用。 - **违背纯函数原则的黑魔法**:每一次 `max_time(3)` 调用都是独立的函数调用,如果它是纯函数,返回结果理应固定不变。它必须依赖隐式状态记录循环的起始时间。码农高天指出作者其实参考过另一种做法:先在外层显式实例化 `t = LimitTimer(3)` 记录起始时间,再重载 `__bool__` 方法在 `while t:` 时检查超时。但原作者“觉得这样写太麻烦了,为什么要多写一行呢?于是他就想尝试用一行来完成这个任务”。

---

[05:00](https://www.youtube.com/watch?v=Jf3IttpFrkw&t=300s) 多例模式实现与“代码屎山”的形成

为了实现单行 `while max_time(3):` 的效果,作者必须在每次调用函数并新建对象时,还能找回第一次运行所记录的开始时间。作者在此引入了**多例模式(Multiton Pattern)**。与保证全局仅有一个实例的单例模式不同,多例模式根据传入的唯一标识符(identifier)维护多个实例。当通过重写 `__new__` 方法寻找 identifier 时,若已存在实例则直接返回缓存的实例,若不存在才创建新实例。

在实现细节上,代码暴露出初学者由于频繁改动思路而未做重构的典型痕迹。Python 的对象生命周期规定,在执行完 `__new__` 之后,即使返回的是已存在的旧实例,Python 运行时依然会强制再执行一次 `__init__`。作者为了防止已经记录的起始时间被冲掉,在 `__init__` 里增加了判断实例是否包含 `initialized` 属性的防御逻辑。码农高天直言:“核心原因就是这个事儿不应该放在 `__init__` 里面实现,你想要的功能很简单,就是在建立这个 instance 时候记录一下他的 start time 对吧,那你为什么不把这件事直接放到 `__new__` 里面呢?”直接在 `__new__` 首次创建分支里记录时间,便可彻底砍掉无谓的 `__init__` 和守卫标志。

此外,类中还残留了完全未使用的 `timeout_seconds` 参数和 `__next__` 方法。这暴露出作者之前尝试实现迭代器协议(如 `while LimitedTime(3):`)走不通后转向新方案,却没有清理废弃代码。码农高天感叹道:“代码里诶,屎山就是这么形成的,所以大家一定要及时清理自己的代码啊,不要让自己的老代码留在自己的文件里。”

作者之所以无法使用单例模式,是因为一旦整个程序自始至终只有一个计时器实例,后续任何地方再次调用 `max_time` 时,都会沿用最初记录的时间戳,导致后续的计时器瞬间判定超时。为了使代码中不同位置的 `while` 循环互不影响,作者必须依赖一套多维度的 identifier 来隔离各自的状态。

---

[10:00](https://www.youtube.com/watch?v=Jf3IttpFrkw&t=600s) 脆弱的上下文标识、C 栈性能损耗与 XY 问题

作者为多例模式设计了一个包含 4 个元素的元组作为唯一标识符(identifier),其命名清晰规范: 1. 线程 ID(`thread_id`):隔离多线程环境下的调用。 2. 运行时调用栈深度(`stack_depth`):试图应对递归函数内部的自调用。 3. 当前代码的文件名(`filename`):结合行号定位调用点。 4. 当前调用的代码行数(`lineno`):定位代码在文件中的具体位置。

看似考虑周全,但码农高天犀利地指出,这套看似严密的 identifier 很容易被简单场景击穿。例如:只要将同一个包含 `while` 循环的 `main` 函数先后运行两次,在相同线程和调用深度下,行号与文件名完全一致,第二次运行就会拿到上一次循环遗留的开始时间,导致功能彻底报废;再或者将该循环嵌套在另一个外层 `while` 循环中,每次外层迭代时这 4 个属性完全相同,同样会引发状态错乱。

更严重的问题在于获取调用栈所带来的性能损耗: - **C-Level Frame 转换开销**:自 Python 3.11 起,运行时的 frame 实际上已经下沉至 C 语言层面(C-level frame)。只有当程序在 Python 代码中显式尝试获取检查栈帧时,解释器才会将其转换为 Python-level frame。 - **严重反客为主**:频繁调用栈检查会导致高昂的性能代价,可能设定循环限时 3 秒,但“实际上只有一秒钟是真正在跑人家的程序,有两秒钟都是在跑你这堆东西”。

```mermaid flowchart LR accTitle: XY 问题思维误区对比 accDescr: 解释作者从原始目标 X 陷入错误路径 Y 并忽视更好设计 Z 与 with 上下文管理器的过程

subgraph RealGoal ["真正需求 X"] X["为代码块/循环添加超时保护"] end

subgraph WrongPath ["陷入的局部路径 Y"] Y1["强制在一行内写成 while func()"] Y2["多例模式 (Multiton)"] Y3["反射调用栈拼凑 Identifier"] Y1 --> Y2 --> Y3 end

subgraph BetterDesign ["更地道的方案 Z / 语义"] Z1["显式两行:先声明 Timer 对象"] Z2["with 语句上下文管理器 (Context Manager)"] end

X -.->|"过度执念"| Y1 X ===>|"符合 Python 直觉"| BetterDesign ```

码农高天总结道,这个项目陷入了软件开发中非常经典的 **XY 问题**:“核心原因是他本身的设计走偏了,就当你的设计出现错误的时候,你的实现是没有办法帮你把设计的问题补回来的。这里是一个经典的所谓 XY 问题,也就是我想要解决 X,于是我需要实现 Y,这个思想根深蒂固之后我就一直在去想我怎么实现 Y,但很多时候我想要解决 X 还有其他的方式,不只是 Y,可能还有 Z,可能还有 ABC,而当你把全部精力集中在如何实现 Y 的时候,你有可能就错过了正确的设计。”

从 Python 语言的直觉与习惯出发,想要界定一段生命周期并做开始/结束的拦截管理,最符合语言特性的语法结构应当是上下文管理器 `with`(例如借助底层信号或定时机制实现),而不是扭曲 `while` 的判定条件。码农高天对作者的代码风格予以认可——私有成员命名规范使用下划线前缀、懂得自定义特定异常类型、格式规整——但最终判定“这是一个很有趣的尝试,但是并不成功”,强调了通过 Code Review 认识自身知识盲区与设计思维差距的重要性。

## Notable quotes > [10:00](https://www.youtube.com/watch?v=Jf3IttpFrkw&t=600s) 当你的设计出现错误的时候,你的实现是没有办法帮你把设计的问题补回来的。 — 码农高天

## Mentioned 码农高天, timeplus, GitHub, Python, 知识星球

## Video description Code Review是提升代码水平最快的方式之一,这期视频我们review一个初学者很短的代码,这个功能可能很多人想过,但是大家能意识到这个设计有什么问题么?

本次代码:https://github.com/songyuc/timeplus

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

中文简报

码农高天指单行循环超时设计存硬伤 栈帧反射拖垮Python性能 执念于单行 while 语法会陷入 XY 问题,通过多例模式和栈帧反射检查超时不仅无法中断循环体内部阻塞,还会因 Python 3.11 栈帧转换带来严重性能损耗,超时控制应改用 with 上下文管理器。 高天审查了一个试图用单行 `while max_time(3):` 语法实现超时中断的开源项目,指出其在循环体阻塞时完全无法生效、无法兼容 `for` 循环等根本缺陷。为了强行达成单行调用的表象,原作者引入了复杂且脆弱的多例模式(Multiton),依赖检查调用栈来拼凑状态标识,不仅在重复调用与嵌套循环中极易崩溃,还在 Python 3.11+ 下带来了极高的 C 栈转换性能损耗。高天总结指出,这是典型的软件开发“XY 问题”——因执着于错误的技术路径而偏离了合理的架构设计,限制代码生命周期更应遵循 Python 直觉,采用 `with` 上下文管理器等正统方案。 码农高天通过对开源项目 timeplus 的 Code Review,深入剖析了试图用单行 while max_time(3) 实现循环超时控制背后的严重设计缺陷与逻辑漏洞。 (0:00) 为在单行内保存循环开始时间,作者引入了基于线程号、调用栈深度及代码行号的多例模式,但极易在函数复用或嵌套循环等基础场景中状态错乱。 (5:00) 自 Python 3.11 起 frame 已下沉至 C 语言层面,循环中高频反射调用栈会导致巨大的性能损耗,出现超时机制反客为主拖慢业务的严重问题。 (10:00) 该案例是典型的 XY 问题,说明当顶层架构设计走偏时,后续的代码黑魔法和实现细节根本无法弥补设计本身的先天不足。 (10:00)

Reading summary

码农高天在对开源项目 timeplus 的 Code Review 中指出,试图通过单行 `while max_time(3):` 实现循环超时控制在逻辑上存在先天缺陷,该项目属于软件工程中典型的“XY 问题”反面教材。 单行 while 超时设计的逻辑硬伤 (0:00) 该项目意图在不使用多线程、多进程或信号机制的前提下,让开发者仅用 `while max_time(3):` 就能控制循环最多跑 3 秒,但这种语法构想在机制上无法自洽。由于超时检查仅在每次 `while` 条件判定的瞬间执行,若循环体内部出现耗时阻塞操作(如 `time.sleep(10)`),程序依然会完整睡眠 10 秒,无法被中途打断。此外,该设计无法直接支持常规的 `for` 循环,开发者必须手动重写为 `while`。每次调用 `max_time(3)` 都是独立函数调用,若保持纯函数特性就无法记录起始时间,因此原作者为了省去显式两行声明(如先建计时器对象再检查)的麻烦,强行引入了隐式状态。 多例模式与缺乏清理导致的实现混乱 (5:00) 为了在单行调用下跨迭代找回初始时间,作者采用了多例模式,通过重写 `__new__` 根据唯一标识符缓存并返回旧实例。然而 Python 在执行 `__new__` 后无论是否为新实例均会强制调用 `__init__`,作者不得不增加 `initialized` 守卫逻辑以防覆盖开始时间;码农高天指出,直接在 `__new__` 的首次创建分支记录时间便可彻底移除无谓的初始化防御。此外,类中还残留了迭代器方案放弃后未清理的 `timeout_seconds` 参数和 `__next__` 方法,生动展示了频繁变更思路却不重构时“代码屎山”是如何累积形成的。 脆弱的上下文标识与调用栈性能损耗 (10:00) 作者设计了结合线程 ID、调用栈深度、文件名与行号的 4 元组作为多例标识符,但码农高天指出该方案极易失效。例如同一个函数若被先后调用两次,或将该循环置于外层嵌套循环中,标识符完全一致会导致第二次运行直接复用上次遗留的时间戳,瞬间误判为超时。更致命的是获取调用栈的性能开销:从 Python 3.11 起运行时栈帧已下沉至 C 语言层面,代码显式检查栈帧会触发昂贵的转换为 Python-level frame 过程。频繁调用可能导致设定的 3 秒限时里有 2 秒都在运行调用栈反射,造成严重的性能反客为主。 执念局部解法引发的 XY 问题 (10:00) 码农高天分析认为,该项目是典型的 XY 问题:开发者原始目标 X 是为代码块添加超时保护,但陷入了必须实现成单行 `while max_time()` 这一局部路径 Y,随后为了修补 Y 不断引入多例模式与栈帧反射等黑魔法,最终越走越偏。从 Python 语言的直觉与习惯出发,界定生命周期的拦截更适合采用上下文管理器 `with`(配合底层信号或定时机制),或者退一步接受显式声明计时器实例的写法。当底层设计出现偏差时,任何表层的复杂实现都无法弥补设计本身的逻辑硬伤。 码农高天肯定了原作者规范的命名习惯与自定义异常意识,但判定这一尝试在架构上并不成功,强调开发者应警惕过度执念于单一语法的局部解法,需通过 Code Review 及早识别设计误区与知识盲区。

Source quotes

[10:00] 码农高天: 当你的设计出现错误的时候,你的实现是没有办法帮你把设计的问题补回来的。

Original transcript

[0:00] 好 又是我们久违了的code review 环节啊这次review的呢依然是我们直指星球的一位用户的代码这个朋友呢还是一个大学生啊刚接触编程不是很久所以这次的案例呢可能对一些刚刚进入编程这个领域的朋友啊有些帮助首先我们看一下这GitHub Repo还是比较青色对吧 比较明显首先啊,PyCash这个文件件肯定是不应该上传的它是一个本地的local cache如果你设置对了正确的Git Ignore这个文件夹就不应该被传上来然后我不确定他希望这个代码是以什么形式存在的我简单看了一下Linlin time.pi是它实现的一个小的类下面这个文件实际上它的一个案例,怎么使用这个类那如果是这么简单的一个结构的话显然我们就完全不需要这个Init的牌对吧如果你要把它打造成一个package 那就是另外一回事那就不应该把你这个文件的放在根目录下这里可以多参考一下其他项目的结构啊我们今天这期视频主要还是聊代码的事儿前面提到了啊这个项目主要是两个文件一个文件是它这个类的实现,一个文件是怎么使用这个类我们这次从这个案例开始看,我们看一下他到底想干什么它这段代码实现的核心内容是max time他想做的功能很直观就是他想限制一个循环运行的时间,那这个需求呢,我想大家可能都有过。我有一个循环对吧他可能干很多任务我希望比如说他最多执行三秒啊三秒之后就不要再继续了那他想象中的语义呢大概是这个样子的这里做一个while max time 3也就是说这个Wave循环最多执行3秒然后里面呢就是一个例子啊这些不重要就这个循环里面干一些事后面呢他就尝试catch这个 run time out error也就是如果超时了啊,raise exception那从语义上看其实有点意思对吧好像有那么一点符合人类的逻辑那我们先去简单的看一下它的实现,然后我们回过头来说的这种设计。他的实现我们先不看细节啊简单的说就是建立了一个counter每一次运行max time这个函数的时候,检查一下时间差如果时间差超了报个错否则的话直接return true我们先不管这个counter本身的实现啊但是大概这个意思大家也能听明白对吧他并没有做很多 fancy 的东西不是什么signal啊multiprocessing啊multithread都不是他写了一个函数 每次运行这个函数的时候检查是不是超时了那你大概了解了一下他的思路之后再回头看这个while max time 3啊你就知道是怎么回事了就是每一次执行这个while loop的时候它实际上都跑了一个max time 3这个函数然后通过这个函数检查超时没超时好为什么我们要说明白这个因为这种设计本身引入了若干的问题第一,我们知道超时检查是只在这个wire loop进行判断的时候实施的那如果我没运行到这个WireLoop就超时了呢?我举个最简单的例子对吧我们这块time点sleep给他休眠10秒那这个歪路是不是能完完整整的运行完十秒的肯定是可以的对吧也就是这个YLUF呢只能运行完一次循环检查一下如果在循环内部大超时了他一点办法都没有但是呢在实际的工程中确实有一些情况是每一次循环我们知道它一定不会超过多长时间的比如我知道每一次循环都是以毫秒为单位的,但是我希望这个循环运行到5秒的时候结束。那这种情况呢确实是这个wire loop可以处理的处理的可以比较好但是我们从另外一个角度想一下由于这个判断是在while loop这块进行的我事实上是没有办法去做for loop 的因为它的检查是在 while 上如果我们想实现一个full loop,那显然这么写是不对的,对吧?他把所有的俘虏都好玩也不会进行检查也就是说想使用它的这种设计,你必须要把for loop给改写成while loop当然在排档中的Full loop确实可以改写成Wile loop他们俩在逻辑上是等价的但是有的时候会比较麻烦对吧除此之外我们也会想到我们在每一次运行max time 3的时候实际上都是运行了一次全新的函数那这个counter本身是怎么计时的呢它是怎么实现的呢因为这个max time 3它不可能是一个纯函数它背后一定有一点点黑魔法它如果是一个纯函数,它每一次的返回值应该是一定的也就是说他在哪里记录的start time那因为这个朋友呢之前在知识星球上咨询过我这个问题啊所以我知道他参考了另外一个人的实现他参考的那个实践是这样的他先建立一个limit timer 3那在建立的时候我们就可以记录下开始的时间然后他去overload这个timer的步方法也就是每一次wild的时候把这个timers变成一个bully变的过程中检查是否超时 如果超时了 报错这个想法的时间路径我们可能会更明确 [5:00] 我们知道它是怎么实现的可是朋友觉得这样写太麻烦了为什么要多写一行呢于是他就想尝试用一行来完成这个任务我们来看看它这个一行的方式是如何实现的 以及这个实现的背后有没有一些问题回到我们刚才看过的这个Math Time函数通过我们刚才的分析,我们已经知道了,每一次while loop的时候这个max time 函数都会被执行一下而且传进来的只有这个timeout时间t在每一次运行的时候,我们都会建立一个limited time object把它复制给Counter然后把现在的时间和counter的start time做比较哎 问题就来了我们如果每一次运行函数的时候都重新建立一个limited time counter他怎么会知道START TIME呢?这是他的实现方法和他参考的实现方法相比最大的区别对吧那可能有的朋友脑海中现在已经浮现出来了一个单词啊没错啊 它这个实现呢叫做Multi-Tune名字听着很怪啊中文叫做多力模式它非常像单立模式但是它一个class可以对应若干个instance我们看这里new的实现也是一个经典的singleton的实现如果传进来的这个identifier在我的多力中找不着就建立一个否则就把现在已经建立的返回通过这种方式就使得尽管这个wild loop运行了非常多次这个max time函数运行了非常多次但是只有第一次运行的时候这个startime被建立了这里我们也看一下他记录这个star time的方式啊它这里呢是overload了init啊,它去判断这个object本身有没有initialize这个attribute如果有代表已经initialize过了啊如果没有呢就重新initialize那这里给大家稍微普及一下啊Python这种单例或者多例模式的实现,在每一次运行完new之后,尽管你return是一个已经建立的instance他还是会运行一下引力坛数这也就是为什么他要建立这个initialize的保护但是这里呢他显然留下了一些他尝试的痕迹对吧它根本没有给这个LimitedTimeClass传进来这个TimeoutSeconds它传进来是一个identifier那这个部分就属于恰好没报错他的这个timeout seconds实际上完全没有用到包括下面这个next实际上它完全没有用它应该是之前尝试了一种实现方式觉得不好后来又换了一种实现方式就导致他现在留了好多老代码在这代码里诶 史山就是这么形成的所以大家一定要及时清理自己的代码啊,不要让自己的老代码留在自己的文件里这个代码一看就是他当时在尝试做这个while limited time 3看看这种形式能不能成后来发现成不了然后他就换了一种实现所以我们就不管这个部分啊我们回过头来说一下这个init大家有没有发现他这个Init写的好像有点别扭,就为什么还要回过头去重新再去做判断啊这些事情?核心原因就是这个事儿不应该放在init里面实现你想要的功能很简单就是在建立这个 instance 时候记录一下他的 start time 对吧那你为什么不把这件事直接放到new里面呢就你直接把这个第16行加上去你的营业函数就是完全没有用的你就是建立的时候记一下时间你也不需要保留initialize attribute这个是一个程序上的小建议啊当然大家现在可能很有疑问的 我们之前一直没有提到的 就是这个identifier他是怎么选择这个 identifier的他为什么选择用了多立模式而不是单立模式他的这个选择 他的这个实现解决了什么问题 有没有遗留下什么bug我们之所以把它留到最后啊是因为它最复杂最难搞明白我们先回到他给的例子说一下为什么单立模式在这里不可以其实原因非常简单单立模式就是只建立一个object 对吧也就是自始至终我只有第一次运行max time这个函数的时候建立一个object记录他的start time那这是不是就意味着如果我在这个函数后面再用一次这个max time 它就不好使了他这里实际上自己给了一个例子就如果你在运行第一个max time的时候,记录了我开始的时间,比如说是第5秒。那你程序运行半天了好 现在你这块来了第二个max time如果你是单立模式的话你依然会认为开始的时间是5秒但是现在距离刚才你那个第一个MagSive已经过了一个小时了那这Max Time 100%不Work对吧这就是为什么他要引入多力模式他希望在每一个while上建立一个object也就是起到我们刚才说的那种先建立一个timers然后while那个timers效果 这是他最终目的那能不能达成这个目的全看它identifier的实现对吧我们来看看他是怎么完成的在这个identifier里面他一共放了四个东西命名非常清晰这个值得表扬四个东西分别是现成的ID运行使得这个站的深度这段代码的文件名 [10:00] 及运行的这个代码的行数那文件名跟行数确定代码位置对吧通过这两个东西我们就解决了刚才我们提到的那个两个不同位置的while可能彼此起冲突的问题这样这两个while就会有自己的instance,不会起冲突了运行时站的深度是什么意思呢?我猜测他可能考虑到了地龟如果出现地龟的情况但是我这个函数调用我自己啊那这个文件名包括这个行数都一样了不是也会起冲突吗于是他就使用了这个站的深度ThreadID也就是现成ID他就肯定想到了多现成的情况如果两个线程都运行这个函数,是不是也要分开?把这些情况都包含到一起他要整了这四个东西放到一个tuple里然后作为他们的identifier问题是这样够了吗也就是说我们能不能举出一个简单的例子让这四个东西都相等然而是不同的两个while loop太简单了对吧我们把Main运行两次就可以了呀那在我们第一次运行main和第二次运行main的时候同样在这个文件里第16行的这个while loop是不是会有同样的现成ID是不是会有同样的站的深度是不是其实是两个WiLoop所以在第二次运行main的时候是不是会直接拿到上一次那个max time的start time那这个东西是不是就不work那事实上我们可以把这个反例做的更简单啊我们只要把这个wire loop放到另外一个wire loop里面就可以了在每一次wire loop执行的时候是不是那几个数据都一样事实上呢,寻找这个identifier的任务是一个基本不可能的任务或许如果你对Python了解的足足够深你可能能想办法解决这个问题但是解决问题的代价也是巨大的大家可能不知道就这两件事的代价就已经非常高了有机会大家可以跑一下Benchmark因为从python3.11版本开始Python运行时的frame实际上是C-level的frame只有你在尝试去获取它的时候它才会把它转换成python level的frame也就是说这个部分是有一定的性能损耗的那没准你现实他跑三秒钟实际上只有一秒钟是真正在跑人家的程序有两秒钟都是在跑你这堆东西为什么会出现这些问题核心原因是他本身的设计走偏了就当你的设计出现错误的时候你的实现是没有办法帮你把设计的问题补回来的这里是一个经典的所谓xy问题也就是我想要解决X 于是我需要实现Y这个思想根深蒂固之后我就一直在去想我怎么实现外但很多时候我想要解决X还有其他的方式不只是Y 可能还有Z 可能还有ABC而当你把全部精力集中在如何实现Y的时候,你有可能就错过了正确的设计。那这个设计本身从Python的感觉来说就是不太对的因为你没有办法直观的感受到它在哪开始计时了他每一次的Wall loop跑的都是同一个函数那如果真的是用黑科技,这个部分怎么写更符合python的感觉呢?这里应该是个with,对不对?这里是个with才符合Pyton的预感但是用 width 怎么实现那就是另外的故事了这里实际上有不少黑魔法可以做到相似的效果但是从语义上说这个width显然是要比那个while更舒服的今天这段代码的代码量其实不大,很少从它的代码风格来说其实是比较规矩的换句话说,简单的事情它完成的很好比如这里的private member 前面加下方见包括他能意识到应该建立一个完全属于自己的exception这边的整体代码风格 这都没什么问题但是由于经验的欠缺,或者是这个知识储备上的不足它在设计方向上明显出了大问题但这也是我们code review 的意义帮助大家从各个方向上进行提升,让你能意识到自己的差距在哪里。所以总体来说我对这个项目的评价是一个很有趣的尝试但是并不成功好了这就是这边的这里喜欢的话就点下关注别忘了一键三连哦字幕志愿者 杨茜茜