# 为什么不用静态分析来实现 HMR (July 12, 2025)

几个月前，我看到 [How to build Hot Module Replacement in Python](https://www.gauge.sh/blog/how-to-build-hot-module-replacement-in-python) 这篇文章，讲的是用他们的 [Tach](https://github.com/gauge-sh/tach) 这个 Python 静态分析工具，可以获得一个依赖图，类似这样的键值格式：

```json
{
  "a.py": ["b.py", "c.py"],
  "b.py": ["d.py"],
  "d.py": ["e.py"],
  "c.py": ["f.py"]
}
```

> 说起来，这个项目上个月开始 unmaintained 了 😅 [一问](https://github.com/gauge-sh/tach/issues/791)才发现原来是开发者跑去 AI 创业了。我觉得网友说的挺好：  
I guess that's the issue with open source tools being backed by VCs. These tools will never be maintained if you can't make tons of money off of them.
>

然后在每次检测到某个文件更改时，依照依赖图更新（调用 import）它和它所有直接、间接影响的模块，这就时它的大致原理。

### 问题
Python 开发者通常不喜欢把文件分得太细，所以事实上如果按照它这套技术方案的话，往往每次重载要涉及的文件会很多（甚至任何一个文件都会让整个项目的每个模块都重载）。使用这套解决方案相比通常的冷重载，其实只省下了 import 第三方库的时间和启动 Python 本身的 overhead。这也让这个所有中间状态都丢失了。

另一方面，懒加载和动态的加载也没法通过静态分析获得。

### 我们的运行时方案
`hmr`采用截然不同的办法，在运行时记录依赖关系。与`tach`的静态分析的方式相反，`hmr`不通过 AST 去分析依赖，而是记录每次 import 了哪些 module，以及用了那个模块中的哪些变量，来决定依赖图。

举个例子，一个文件`data.py`如下：

```python
a = 1
b = 2
c = a + b  # 3
```

然后有个文件`a.py`使用了`data.py`的值：

```python
from data import a
print(a)
```

如果是静态分析，会认为`a.py`依赖于`data.py`，如果`data.py`重载了，则`a.py`也应该重载。

而我们的`hmr`则支持“更细粒度的反应式”，什么意思呢，它认为`a.py`只依赖于`data.py`中的`a`的值。所以如果这时候更改`a.py`的`b = 2`为`b = 3`，这时候`data.a`完全没有改变，所以`a.py`并不会重载。

只有当你修改了`a = 1`为别的值时，才会触发`a.py`重新打印 a 的值。同理，如果你的入口文件是`c.py`其内容如下：

```python
import data
print(data.c)
```

那么当你改变`a`或者`b`的时候（只要`a + b`的结果变了）那 c 就会变，导致重新打印出`data.c`出来。

<img src="/nlark/yuque/0/2025/webp/26070246/1752397662001-c8003f2b-4bd0-4360-a89e-349a9d4859dc.webp" width="660" title="" crop="0,0,1,1" id="u0372c791" class="ne-image">

这就是静态分析永远难以企及的细粒度热重载。

### 原理
其实很简单，本质上，我只要让每次某个 module 被`__getattr__`了都记录下来就行了。比如，如果`a`模块触发了`b.__getattr__("c")`，意味着`a.py`中肯定存在`from b import c`或者`b.c`这样的地方，那么`a`模块就依赖于`b.c`。

至于如何自定义 module 的`__getattr__`，就是普通的元编程了（我在 [这篇](/blog/yfw3tf2h1mzghaz7) 文章中详述过）。简单来说就是实现一个自定义的 ModuleType 子类，然后在`sys.meta_path`中注册一个 ModuleFinder 就行了。

### 结语
经过大半年的努力，我真的处理了超级多 edge cases，解决了很多 performance issues，目的就是让所有的魔法都 work transparently，让所有的 dragon 都不用被用户影响。

现在 hmr 作为一个 CLI 已经完全模仿 python 的行为了。比如你平常以`python main.py -a --b c`这样启动的应用，直接改成`hmr main.py -a --b c`这样，所有事情应该 just work ~

如果你用 uvicorn 启动 Web 服务，你也可以试试看 uvicorn-hmr，它也是一个 drop-in replacement of `uvicorn --reload`，直接替换命令即可。我在向后兼容上做的非常小心

包名同 CLI 名，比如 hmr 就是`pip install hmr`，uvicorn-hmr 就是`pip install uvicorn-hmr`

---

最近还做了个有趣的更 hack 的工具，`hmr-daemon`，它的行为是，`pip install hmr-daemon`后，什么代码都不用改，但是当你修改某个文件而且此时 python 进程还没退出的话，这个模块就会重载，当然它影响到的模块也会按正确的顺序重载。对于在 REPL 比如`ipython`中使用你项目中的模块时很有用。