跳转至

HMR - An Engine, a Tool, an Ecosystem.

一个 反应式编程 引擎 + 一个 热重载 框架。致力成为 Python 界的 Vite

Repo   ·   Home   ·   Docs   ·   PyPI

HMR provides a pythonic, flexible, progressive-yet-intuitive reactive programming engine/framework, and on top of that, a fine-grained, on-demand hot-reload tool.

  • 这个反应式框架实现了 push-pull reactivity:Signal, Derived, Effect
  • 这个热重载工具提供了即插即用的 CLI 工具直接替代 python、uvicorn、mcp/fastmcp

由于 reactive programming 懂的人自然懂,不懂的人说明用不上,所以下面可能更多 demo 集中在热重载功能。

比如下面这是前段时间做的 mcp-hmr 的 demo

下面是更早录的 FastAPI 的例子,实现类似 uvicorn --reload 的重载,但是快得多,更环保节能:

由于当时还没发布 uvicorn-hmr

所以视频中看到的是有一个折叠了的 main.py 中的 start_server 函数,启动命令是 hmr main.py。现在不一样了。现在可以直接 uvicorn-hmr main:app 启动,不需要对代码有任何改动。可以说,任何可以被 uvicorn 正常启动的 ASGI application 都可以直接用 uvicorn-hmr 启动,零门槛享受热重载的开发体验。

为什么要有 HMR

说起来,我在杭州 实习 的时候,杭州的老板 看到我写的 Python 服务用 uvicorn 启动时居然只有冷重载,感到十分惊讶。我们对 Python 生态的 DX 感到十分遗憾。这段经历在我内心种下了一个种子

传统的 冷重载 工具(比如 watchfiles CLI 或 uvicorn 的 --reload)本质上只是:检测文件变化,杀进程,从头重启进程。这对加载 ML 模型、建立数据库连接这类昂贵初始化来说是噩梦。

热重载 则不重启进程,而是在原进程上修修补补 ,智能地根据受改动的部分 按需重算:

  1. 直接修改的 python 模块会被重新执行(如果是被 import 了的模块)
  2. 这个模块声明的变量如果有变化,则会触发 “依赖这个变量” 的代码重算
  3. 一直传递下去,直到不再有受影响的代码仍未刷新的

甚至不仅保存 python 代码会触发这样的反应式细粒度热重载。如果你的代码 read 了某个文件,则改动那个文件(即使不是 .py 文件)也可以触发对应代码 rerun

这样神奇的功能都是通过反应式编程或者说 signal pattern 实现的,而不是静态分析。者带来了诸多好处,详见 下文。我另外也有一篇博客:为什么不用静态分析来实现 HMR / [csdn] / [dev.to] / [medium]

HMR 生态

包名 用途
hmr 反应式编程库和热重载核心实现
hottest 🚧 支持热重载的测试框架!想想 Vitest!
uvicorn-hmr 为 ASGI 应用提供热重载的 Uvicorn 服务器
mcp-hmr 为 MCP / FastMCP 服务器启用热重载
hmr-daemon 后台守护进程,监控文件变化自动刷新模块
fastapi-reloader FastAPI 浏览器自动刷新中间件

双重身份

作为反应式引擎

HMR 底层是一套受 Svelte 和 SolidJS 启发的 push-pull 反应式框架:

from reactivity import signal, derived, effect

count = signal(100)  # data source

@derived
def odd():
    return count.get() % 2

@effect
def print_result():
    print(f"{odd() = }")  # effect reruns when dependencies change

# initial run prints: odd() = 0
count.set(1)  # triggers effect, prints: odd() = 1
count.set(101)  # odd() still 1, effect not rerun
count.update(lambda v: v + 1)  # now odd() = 0, effect reruns and prints: odd() = 0

核心三元素:

  • Signal:可观测的数据源,被订阅者们监听
  • Derived:派生值,从其它 Signal / Derived 计算而来,惰性求值且自动缓存
  • Effect:副作用,当依赖变化时自动 rerun

这套设计的精妙在于"push-pull 混合":Signal 推送变化通知,Derived 被 Effect 按需拉取,既避免了不必要的重算,也不用轮询。加上 async_effect、async_derived 和 new_context() 隔离,你可以在 Python 里实现任何现代 JS 框架的反应式能力。

作为热重载工具

元编程也可以不 hacky —— HMR 完全基于非 monkey-patching 的 API,比如注册 import 钩子等,以最大化兼容性和防御性编程。其原理大致如下

  1. 通过 sys.meta_path 拦截所有 import(不改你的代码)
  2. 把模块的命名空间包装成反应式 proxy,每次 __getattr__ 都记录 “谁访问了哪个模块的哪个变量”
  3. 另用 sys.addaudithook 监控其它文件读取,每次 open 都记录 “谁访问了哪个文件”
  4. 文件变化时,watchfiles 通知我们,只重新执行受影响的部分(如上所述)

我干了些什么骚操作

这一段是 AI 写的,包括这个小标题也是 AI 起的

变量级细粒度

大多数热重载工具是模块级的。我做到了变量级。你改了某个函数的实现,只有真正调用它的代码会重算。改了模块中你没用的常量?什么都不会发生。

push-pull 反应式

纯 push 会导致级联重算的浪费,纯 pull 要轮询。HMR 用"invalidation push + lazy recompute":Signal 推送变化,Derived 等到被需要时才重算,而且结果还能缓存。

绕过 CPython 限制

我曾给 CPython 提过 一个 issue:在类定义里访问全局变量时,如果 globals 是 dict 的子类(而不是纯 dict),就会出错。HMR 用 AST 变换优雅地绕过了这个坑,让函数级热重载能支持在函数里写 class。这个还挺复杂的,所以我写了一篇博客讲这个故事:[blog] / [csdn] / [juejin]

.pth 文件黑科技

hmr-daemon,会往 site-packages 扔个 .pth 文件,不改你一行代码,普通的 python script.py 启动也能在后台热重载。这是我最喜欢的"对 DX 的小关怀"。我曾经写过它的故事:[blog] / [csdn] / [juejin] / [medium]

用法

CLI

最直白的方式:

hmr main.py arg1 arg2
# or
hmr -m package.module

参数什么的跟 python 命令一模一样,只是现在文件改动会自动热重载。

uvicorn 集成

用 uvicorn-hmr 替代 uvicorn CLI,给你的 FastAPI / 任意 ASGI 应用加上真正的热重载体验:

uvicorn-hmr main:app
# or
uvicorn-hmr main:app --refresh  # --refresh 还能自动刷浏览器

daemon thread

装 hmr-daemon,然后任意方式启动 Python(python script.py、进入 IPython REPL、运行其它 CLI 工具)都会自动支持热重载。通过 .pth 文件在启动时注入,对代码零改动。

MCP Server 集成

mcp-hmr 是一个 MCP 服务器,把 HMR 的文档和源码暴露给 Claude、Cursor 这些工具,让它们能直接查文档。

Comparisons

由于该项目的双重生态位,下面将分别与相关项目进行比较。

热重载工具

传统的冷重载工具如 watchfiles CLI 或 uvicorn --reload 只是检测到文件变化就重启整个进程,无法保留状态。

Tach 用静态分析 AST 来追踪模块依赖,做到了模块级重载和状态保留,但只能监控 .py 文件。比起来,HMR 在运行时追踪依赖,能做到变量级细粒度 —— 当你 from module import name 时,我能精确记录"谁访问了哪个模块的哪个变量"。如果 name 本身没变,就不触发重载。这比静态分析(它只知道"模块 A 导入了模块 B")能掌握更细的细节。

jurigged 和 reloadium 也实现了运行时追踪,但都依赖 monkey-patching,兼容性有限。HMR 用 sys.meta_path 钩子和自定义 ModuleType 来实现,完全避免了 monkey-patch 的坑。

另外,只有 HMR 能监控任意文件。你的代码读了某个 YAML、TOML、JSON 吗?改那些文件也会智能地重新执行依赖它的代码。这种全文件系统的追踪,加上变量级的细粒度,让 HMR 在实际项目中快得飞快。

反应式编程框架

框架内置反应式系统的,比如 Textual 和 Shiny for Python,通常只支持异步反应式。HMR 则同时支持同步和异步反应,而且完全框架无关。

marimo 和 IPyflow 这类交互式笔记本用静态分析来追踪单元格的依赖,有同样的局限性。RxPY 更多是个事件驱动框架,不是细粒度反应式引擎。

设计上,HMR 的 API 参考了 Svelte、SolidJS、Vue 这些现代前端框架。目标就是在 Python 里实现 Vite 那样的开发体验。

深入了解

想玩得更溜?去 官方文档 看 Signal、Derived、Effect 的高级用法,或者看 源码——代码写得自我说明,注释也很有料。一个 interactive 的 docs site 也正在建设中

For AI Assistants

还可以装个 hmr 文档的 MCP Server,让 Claude 或 Cursor 帮你读代码:

Install MCP Server

或者在你的 IDE 配置中添加这个 MCP 服务,就能让 AI 助手直接查 HMR 的文档和源码。

{
  "mcpServers": {
    "hmr-docs": {
      "type": "http",
      "url": "https://py3.online/hmr/mcp"
    }
  }
}

此外我们还有 llms.txt


其它资料

最初实现的最原型的版本(当时还是一个单文件脚本,甚至还没发布),录了个视频:[bilibili]

然后前段时间又录了个关于 uvicorn-hmr 细粒度 invoke 浏览器刷新的视频(这个功能现在还没发布):[bilibili]