跳转至

以下内容基本上是 AI 生成的,我还没校对,可能质量不高

HMR vs reaktiv:谁有更好的 DX

HMR Repo   ·   reaktiv Repo   ·   HMR Reactive Guide

TL;DR: reaktiv 是个优秀的反应式库,但 API 设计不够符合 Python 使用习惯。HMR 通过 更人体工学的装饰器语法更符合直觉的自动生命周期管理更直观的命名提供了更好的开发者体验

体验 HMR 的反应式编程

引言:API 设计的哲学差异

在 Python 反应式编程领域,reaktivHMR 都是优秀的解决方案。两者都提供了信号 (Signal)、派生值 (Derived) 和副作用 (Effect) 这三大基石。

但在 API 设计上,我们走了完全不同的路。reaktiv 作为 FRP (Functional Reactive Programming) 的忠实实现,使用类作为原语,这虽然符合理论本质,但不够符合 Python 开发者的使用习惯。HMR 则更注重 Pythonic 的体验。

核心差异

reaktiv: FRP 理论导向,严格地类作为原语,功能完备但学习曲线陡峭

HMR: Python 体验导向,装饰器语法,简洁直观易上手

1. 生命周期管理:垃圾回收的诅咒

这是最让开发者头疼的问题。让我们看看两者的对比:

reaktiv 的噩梦

from reaktiv import Signal, Computed, Effect

counter = Signal(0)
...
doubled = Computed(lambda: counter() * 2)

# ❌ 致命错误:Effect 立即被垃圾回收!
Effect(lambda: print(f"Counter: {counter()}, Doubled: {doubled()}"))

counter.set(1)  # 什么都不会发生,因为 Effect 已经死了

reaktiv 要求 手动管理 Effect 的生命周期

# ✅ 正确方式:必须赋值给变量
my_effect = Effect(lambda: print(f"Counter: {counter()}"))

# 或者塞到列表里
effects = []
effects.append(Effect(lambda: print("Another effect")))

这种设计源于性能考虑——防止内存泄漏。但代价是开发者体验的灾难。

HMR 的优雅解法

from reactivity import signal, derived, effect

counter = signal(0)
doubled = derived(lambda: counter.get() * 2) #(1)!

# ✅ 自动管理:Effect 永不被垃圾回收
@effect
def print_values():
    print(f"Counter: {counter.get()}, Doubled: {doubled()}")

counter.set(1)  # 立即响应:Counter: 1, Doubled: 2
  1. 派生的信号在 reaktiv 中叫 Computed,在 HMR 中叫 derived

HMR 通过 装饰器语法智能引用追踪 ,自动管理生命周期。你永远不需要担心 Effect 被意外回收。

为什么 HMR 更好

自动管理 = 零心智负担。开发者只需关注业务逻辑,不用操心基础设施。

3. 异步支持:原生 vs 实验性

reaktiv 的异步支持

reaktiv 对异步的支持是 实验性的

import asyncio
from reaktiv import Signal, Effect

my_signal = Signal("initial")

# ✅ 成熟的异步支持(v0.19+)
async def async_effect():
    await asyncio.sleep(0.1)
    print(f"Async processing: {my_signal()}")

# 必须赋值!
my_async_effect = Effect(async_effect)

官方文档明确警告:"Async effects are experimental and should be used with caution."

reaktiv 异步支持的限制

reaktiv 提供了 to_async_iter() 函数将信号转换为异步迭代器,但 存在严重的设计限制

  • to_async_iter() 使用了 asyncio.Queue,导致 无法在 trio 中正常工作
  • 这意味着如果你使用 trio 作为异步框架,reaktiv 的异步功能将不可用
  • 错误信息:"trio.run received unrecognized yield message \<Future pending>"

HMR 的原生异步支持

HMR 从一开始就设计了 完整的异步支持

import asyncio
from reactivity import signal, async_effect

my_signal = signal("initial")

# ✅ 原生异步支持
@async_effect
async def process_data():
    await asyncio.sleep(0.1)
    print(f"Processing: {my_signal.get()}")

# 运行异步效果
async def main():
    await process_data.trigger()  # 手动触发
    my_signal.set("updated")
    await process_data.trigger()  # 再次触发

asyncio.run(main())

不仅如此,HMR 还支持异步计算值:

from reactivity import signal, async_derived

@async_derived
async def fetch_user_data():
    # 异步计算逻辑
    data = await api_call()
    return process_data(data)

# 使用方式
async def example():
    result = await fetch_user_data()  # 必须调用才会执行
    print(result)

为什么异步重要

在现代 Python 应用中,异步操作无处不在。原生异步支持意味着更好的性能和更自然的代码风格。

4. 上下文隔离:架构灵活性

reaktiv 的全局状态

reaktiv 的所有反应式对象共享全局上下文。这在简单应用中没问题,但在复杂应用中可能导致问题:

# reaktiv: 所有信号都耦合在一起
from reaktiv import Signal
app1_counter = Signal(0)
app2_counter = Signal(0)

# 难以隔离不同模块的状态

HMR 的上下文隔离

HMR 支持 独立的反应式上下文

from reactivity import signal, new_context

# 创建独立的上下文
ctx1 = new_context()
ctx2 = new_context() #(1)!

# 每个上下文都有自己的信号
counter1 = signal(0, context=ctx1)
counter2 = signal(0, context=ctx2)

# 上下文之间完全隔离
  1. 事实上,后文提到的热重载,就拥有自己独立的上下文。因此热重载的模块变量的信号与 batching 对使用 reactivity 模块的用户创建的信号是透明的(隔离的),不会互相干扰。

这让 HMR 适合构建复杂的应用架构:

# 不同模块可以有独立的反应式系统
user_context = new_context()
admin_context = new_context()

# 甚至可以构建自己的反应式框架
class MyReactiveFramework:
    def __init__(self):
        self.context = new_context()

    def signal(self, value):
        return signal(value, context=self.context)

架构优势

上下文隔离让 HMR 适合构建大型应用和框架,而不仅仅是简单的反应式编程。

5. 热重载集成:开发体验的飞跃

这是 HMR 最大的优势之一:反应式引擎与热重载工具深度集成

reaktiv 的开发体验

# reaktiv: 修改代码后需要重启应用
# 失去所有运行时状态

HMR 的开发体验

# HMR: 代码修改后自动热重载
# 保持应用状态,立即看到变化

hmr main.py  # 启动应用
# 修改代码,立即生效,无需重启

不仅如此,HMR 的反应式系统在热重载时会正确重建依赖关系:

# 热重载时,反应式关系自动重建
@derived
def expensive_calc():
    # 复杂的计算逻辑
    return heavy_computation()

# 修改 heavy_computation 函数
# HMR 自动重新计算依赖它的所有值

开发效率提升

HMR 将反应式编程的开发体验提升到新高度。代码修改后立即生效,保持应用状态,这在开发复杂应用时特别重要。

6. API 命名的一致性

reaktiv 的命名风格

from reaktiv import Signal, Computed, Effect
# 首字母大写,Java 风格

HMR 的命名风格

from reactivity import signal, derived, effect
# 全小写,Python 风格

HMR 的命名更符合 Python 惯例:

  • signal 而不是 Signal
  • derived 而不是 Computed
  • effect 而不是 Effect

这种小写风格与 Python 内置函数(如 len, sum, max)保持一致。

7. 高级特性:简洁 vs 复杂

reaktiv 的高级特性

reaktiv 提供了丰富的功能,但 API 相对复杂:

from reaktiv import Signal, Computed, Effect, LinkedSignal, batch, untracked

# LinkedSignal - 可写的计算值
selection = LinkedSignal(lambda: f"default-for-page-{page()}")

# 自定义相等性
items = Signal([1, 2, 3], equal=lambda a, b: a == b)

# 批量更新
with batch():
    name.set("Bob")
    age.set(31)

# 选择性依赖追踪 - 读取信号但不创建依赖
debug_mode = Signal(False)
if untracked(debug_mode):  # 不创建依赖
    print("Debug mode is on")

Effect 清理函数(管理资源):

def database_effect(on_cleanup):
    connection = open_database()

    def cleanup():
        connection.close()  # 释放资源

    on_cleanup(cleanup)

db_effect = Effect(database_effect)

to_async_iter(转换为异步迭代器):

from reaktiv import to_async_iter

# 将信号转换为异步迭代器
async for value in to_async_iter(counter):
    print(f"Counter: {value}")

reaktiv 异步迭代的限制

to_async_iter() 使用了 asyncio.Queue,这导致:

  • ❌ 无法在 trio 中正常工作
  • ❌ 只能与 asyncio 一起使用
  • ❌ 与其他异步框架不兼容

HMR 的高级特性

HMR 同样功能强大,但 API 更简洁:

page = signal(1)
@derived
def selection():
    return f"default-for-page-{page.get()}"

# 简洁的批量更新
name = signal("Alice")
age = signal(25)
with batch():
    name.set("Bob")
    age.set(31)

简洁不等于功能少

HMR 通过更好的基础设计,在保持强大功能的同时提供更简洁的 API。

性能对比:谁更快?

有趣的是,HMR 的设计不仅更简洁,在某些场景下还更高效:

细粒度更新

HMR 的 push-pull 混合模型确保只重新计算真正需要更新的值:

a = signal(1)
b = signal(2)
c = derived(lambda: a.get() + b.get()) #(1)!
d = derived(lambda: a.get() * 10)  #(2)!

a.set(2) #(3)!
  1. 只依赖 a 和 b
  2. 只依赖 a
  3. 只重新计算 c 和 d,b 的依赖者不受影响

懒求值

HMR 的计算值是懒求值的,只有在需要时才计算:

@derived
def expensive_calc():
    return sum(range(1000000))  # 只有调用时才计算

# 不会立即计算
result = expensive_calc  # 只是函数对象

# 只有读取时才计算
value = expensive_calc()  # 现在才计算

结论:两种优秀方案的不同哲学

reaktiv 和 HMR 都是优秀的反应式编程库,各有千秋。reaktiv 作为 FRP (Functional Reactive Programming) 的忠实践行者,使用类作为原语,这虽然在理论上更纯正,但确实不够符合 Python 开发者的直觉。HMR 则更注重 Pythonic 的体验。

特性维度 reaktiv HMR
生命周期管理 手动管理(需赋值给变量) 自动管理(装饰器语法)
编程风格 面向对象(类作为原语) 声明式(装饰器语法)
异步支持 仅 asyncio(不兼容 trio) 原生完整支持(兼容 trio/asyncio)
上下文隔离 全局状态 独立上下文
热重载集成 深度集成
API 命名 首字母大写(Java 风格) 全小写(Python 风格)
学习曲线 较陡峭(理论导向) 平缓(体验导向)
内存管理 开发者控制,易泄漏 智能追踪,自动管理
代码简洁性 功能丰富但复杂 功能强大且简洁
调试体验 类实例追踪 命名函数,更直观
高级特性 LinkedSignal、untracked、batch 内置装饰器,简洁直观
异步迭代 to_async_iter(仅 asyncio) 原生 async 支持

1. 零心智负担的生命周期管理

  • 自动垃圾回收,无需手动赋值
  • 装饰器语法让代码更清晰

2. 声明式编程风格

  • 装饰器语法支持命名函数
  • 更好的调试和测试体验
  • 符合 Python 最佳实践

3. 原生异步支持

  • 从设计之初就支持异步
  • 完整的 async/await 生态

4. 架构灵活性

  • 上下文隔离支持复杂应用
  • 适合构建框架和大型系统

5. 热重载深度集成

  • 开发体验的质的飞跃
  • 保持运行时状态

6. Pythonic 设计

  • 小写函数名符合 Python 惯例
  • 简洁而不失功能

选择建议

选择 reaktiv 如果:

  • 你是 FRP 理论的拥趸,希望严格遵循学术定义
  • 你需要精细控制每个反应式原语的生命周期
  • 你已经在使用类似的类-based 反应式库
  • 你只使用 asyncio 作为异步框架
  • 你需要高级特性如选择性依赖追踪

选择 HMR 如果:

  • 你希望更符合 Python 习惯的 API 设计
  • 你重视开发体验和代码简洁性
  • 你需要热重载和现代异步支持
  • 你使用 anyio / trio
  • 你希望智能管理生命周期,减少样板代码

两者都是优秀的选择,选择取决于你的哲学偏好和项目需求。reaktiv 体现了 FRP 的理论纯正,HMR 则体现了 Python 的实用主义。如果你需要异步编程支持,尤其是 trio,HMR 是唯一可行的选择。


想体验 HMR 的简洁 API? 立即开始使用 HMR | GitHub 仓库 | 了解更多关于在 Python 中的反应式编程

API 设计的哲学

好的 API 设计应该既符合理论本质,又贴近开发者习惯。reaktiv 体现了 FRP 的理论纯正,HMR 则体现了 Python 的实用主义。