pyochain
pyochain 是一个 Python 库,提供具有流畅 API 的各种类,用于处理迭代、集合、处理可选值、管理错误等!
注意: 本文档与 dev 分支保持同步,可能包含尚未在最新稳定版本中发布的功能。
目录
主要特性
- 使用
Option[T]处理可选值,而非T | None。 - 使用
Result[T, E]处理成功和错误路径,而非try.. except块。 - 针对 Python 内置类型(
zip、map、...)和itertools的Iterator类型,以及 RustIterator中的许多方法,还有toolz或more-itertools等库。 - 覆盖 Python 内置类型(
list、deque、set、...)的Collection类型,额外的无拷贝切片视图,以及通过两个专用类型(HeapMax和HeapMin)重新实现的heapq模块。 - 模仿
collections.abc的ABC层级结构,用于鸭子类型、共享方法,并允许实现自定义子类。 - 可添加到任何类的
Mixin,通过pipe和tap提供流畅 API,或在真值评估时进行Option/Result转换。
核心设计原则
- 使用 Rust 编译,借助 Pyo3 实现最大性能。
- 流畅 API 设计用于链式调用方法,使代码阅读如同阅读书籍 => 从上到下,从左到右。
- 一等静态类型支持:针对
Option和Result类型的泛型、重载和模式匹配。
这就是它被称为 pyochain 的原因:它允许你构建针对数据的操作链,代码通过 Pyo3 在 Rust 中编译。
安装
Pyochain 支持 Python 3.13 及以上版本,并为 Linux、Windows 和 MacOS 提供编译好的 wheel 包。
uv add pyochain # or pip install pyochain
链接
为什么使用 pyochain?
🔥 极速性能
由于采用静态编译,pyochain 在设计上比其他类似的 Python 库快一个数量级。
例如,像 x = Vec([1, 2, 3]) 这样简单的单对象创建,比 Vec 用纯 Python 实现时快 30%。
对于 Iterator 方法和类,这种加速效果更为显著,通常比 more-itertools 的等价实现快 2 到 10 倍。
即使源代码大部分仍是 Python,也付出了巨大努力来优化性能,这或许就是它在该类别中已被评为最快库的原因,参见此对比(此时,只有 Result 和 Option 是编译过的,它们与此基准测试无关)。
🌍 丰富的生态系统
Pyochain 旨在作为许多内置类型和函数的直接替代品,提供广泛的功能,旨在与彼此以及更广泛的 Python 生态系统无缝交互。
未来计划了许多额外的功能,包括 Array、排序集合、唯一可变序列等。
🛡️ 100% 类型安全
IDE 自动补全是一个主要关注点,pyochain 为其所有构造提供了详尽的重载和泛型支持。
在某些情况下,它甚至比 typeshed 更完整,例如 map_star 在参数和返回类型方面完全类型化,而 itertools.starmap 则不是。
这是开发库时最难测试的部分,因此如果您遇到任何类型问题,请报告!
📚 准确、经过测试的文档
每个方法和类型都有详尽的文档,并包含经过正确性测试的可运行示例。
甚至这个 README 也经过了测试!
入门指南
Mixin
Pipe、Tap、Checkable 以及其他 mixin 可以添加到任何类中,以添加新方法,无论底层实现如何。
除了 __bool__ 之外,它们不依赖于内部状态,因此可以普遍应用于任何子类,包括您自己的子类。
pandas、polars 或 Rust crate tap 的用户会对 pipe() 和 tap 感到如鱼得水,而 Rust 开发者则会欣赏 Checkable 类型,它提供了诸如 then、then_some 或 ok_or_else 之类的方法,通过评估实例的真值性来返回相应的 Option 或 Result 类型,就像 Rust 中的 bool 方法一样。
所有 pyochain 类型都继承自它们,这使得针对集合空值或错误处理的控制流成为管道的一部分。
迭代器
Pyochain 具有各种 Iterator 类型,拥有流畅的 API,其功能来自:
- Python
builtins(完全覆盖)=>zip、map、... itertools模块(完全覆盖)=>chain、combinations、...- Rust
std::iter::Iterator=>try_collect、partition、... more-itertools库 =>all_unique、arg_max、tail、...toolz库 =>map_juxt、count、first、...
它们可以用于以可读的方式构建复杂的转换、过滤和聚合管道(无需嵌套循环或推导式),而无需创建中间集合(惰性执行),从而改善性能和内存使用。
许多方法与其 Rust 对应方法行为一致:filter_map 过滤由闭包返回的 Option,find 返回匹配谓词的第一个元素并包裹在 Option 中,等等...
所有与迭代器相关的源代码均以 Rust 实现,并调用内部实现或 CPython 内置函数。
这保证了在性能上没有任何权衡,从迭代器实例化本身到实际的迭代过程。
以下是如何使用 Iter 的示例,以及它与使用 itertools 的纯 Python 实现的对比:
from pyochain import Iter, Seq
import itertools
wanted = ((0, "1"), (1, "9"), (2, "25"), (3, "49"), (4, "81"))
pyochain_res = (
Iter
.from_count(1)
.filter(lambda x: x % 2 != 0)
.map(lambda x: x**2)
.take(5)
.enumerate()
.map_star(lambda idx, value: (idx, str(value)))
.collect(tuple)
)
py_res = tuple(
itertools.islice(
itertools.starmap(
lambda idx, val: (idx, str(val)),
enumerate(
map(lambda x: x**2, filter(lambda x: x % 2 != 0, itertools.count(1)))
),
),
5,
)
)
assert pyochain_res == py_res == wanted
集合
每种 Python 内置集合类型(list、tuple、range、dict、set 等)都有对应的 pyochain 类型,此外还有诸如 SliceView(切片的无拷贝视图)或 StableSet(保持插入顺序的可变集合)等集合,未来还将规划更多类型。
许多方法被设计用于与库的其他部分互操作:Dict::get_item 或 Seq::get 返回一个 Option,Vec::drain 返回一个 PyoIterator,依此类推。
from pyochain import Dict, Iter, Some
from pyochain.collections import StableSet
names = ["Charlie", "Alice", "Bob", "Alice"]
# Create a Dict from an iterator of key-value pairs
data = Iter.from_count().zip(names).collect(Dict)
assert data == Dict({0: "Charlie", 1: "Alice", 2: "Bob", 3: "Alice"})
# try_insert returns a Result, which is Err if the key already exists
err = data.try_insert(1, "David")
assert err.is_err()
# sort return a Vec
vals = data.values().iter().map(str.upper).sort()
assert vals.first() == "ALICE"
assert vals.len() == 4
# Modify the Vec in place with retain according to the predicate
vals.retain(lambda x: x.endswith("E"))
assert vals.len() == 3
# Create a set of unique names, preserving insertion order, with StableSet
unique_names = vals.pipe(StableSet)
assert unique_names == {"ALICE", "CHARLIE"}
assert unique_names.iter().next() == Some("ALICE")
结果与选项
使用专用类型以显式方式处理 None 和异常,而不是依赖隐式真值检查或 try/except 块。
成功和失败可以分别由 Ok[T] 和 Err[E] 类型表示,而可选值可以由 Some[T] 和 Null 表示。
这使每条路径都变得显式、更少出错且更易读,通过用类似 x.map().and_then().unwrap_or() 的单一管道替换嵌套的 try/except 块和 if x is not None 检查。
对于熟悉此模式的用户,其 Rust 对应物中存在的几乎所有方法均可用,此外还提供了额外的便捷方法以兼容广泛的 python 生态系统,例如 unwrap_or_none()(我知道这是异端)。
from pyochain import Option, NONE, Some, Seq, Vec, Set, Ok, Err, Result
from pyochain.abc import PyoIterable
def divide(a: int, b: int) -> Option[float]:
return NONE if b == 0 else Some(a / b)
assert divide(10, 2) == Some(5.0)
# Provide a default value
assert divide(10, 0).unwrap_or(-1.0) == -1.0
# Convert between Collections -> Option -> Result
tup = (1, 2, 3)
seq = Seq(tup)
assert seq.then_some() == Some(Seq(tup))
assert seq.then_some().ok_or("No values").unwrap() == Seq(tup)
# Accept any Pyochain Iterable
def _process(data: PyoIterable[int]) -> str:
return data.iter().map(str).join(", ")
# Process only if non-empty, convert Option to Result
assert seq.then(_process).ok_or("No values").unwrap() == "1, 2, 3"
assert (
Vec(()).then(_process).ok_or("No values").expect_err("expected error")
== "No values"
)
# Create empty Set, convert to Result, then back to Option
assert Set(()).then(_process).ok_or("No values").ok() is NONE
使用模式匹配进行类型安全的穷举处理
类型检查器将确保在匹配 Option 和 Result 类型时处理所有情况。
from pyochain import Result, Ok, Err
def try_parse_int(s: str) -> Result[int, ValueError]:
try:
return Ok(int(s))
except ValueError as e:
return Err(e)
def handle_result(res: Result[int, ValueError]) -> str:
match res:
case Ok(value):
return f"Parsed value: {value}"
case Err(_):
return f"Error parsing int!"
assert try_parse_int("123").pipe(handle_result) == "Parsed value: 123"
assert try_parse_int("abc").pipe(handle_result) == "Error parsing int!"
ABC's
提供了一个模仿 collections.abc 模块的类层次结构,每个类仅需与其标准库对应项相同的 dunders(例如,PyoIterable 对应 __iter__),同时提供了许多受 Rust 启发的附加方法,如 PyoMutableSequence::retain、PyoMutableMapping::try_insert,以及当然还有来自 PyoIterator 的众多功能。
所有具体类型 Iterator 和 Collection 都实现了这些接口,因此您可以使用它们进行类型检查、实现自己的子类,并无缝替换 Python 生态系统中的类型,且无需权衡取舍。
from pyochain import Some
from pyochain.abc import PyoSequence, PyoIterable
from dataclasses import dataclass
from collections.abc import Sequence
@dataclass(slots=True)
class MySequence(PyoSequence[int]):
data: list[int]
def __len__(self) -> int:
return len(self.data)
def __getitem__(self, index: int) -> int:
return self.data[index]
x = MySequence([1, 2, 3])
# Call any method from PyoSequence, like first(), last(), get(), etc...
assert x.get(2) == Some(3)
# Convert to an Iterator and immediately benefit from all the Iterator methods
assert x.iter().map(lambda x: x**2).collect(tuple) == (1, 4, 9)
# Convert to an Option just like pyochain core API
assert x.then_some() == Some(x)
# Works with runtime instance and subclass type checking
assert isinstance(x, PyoSequence)
assert isinstance(x, PyoIterable)
assert isinstance(x, Sequence)
assert issubclass(MySequence, PyoSequence)
assert issubclass(MySequence, Sequence)
稳定性声明 ⚠️
pyochain 目前处于早期开发阶段(< 1.0),在达到稳定的 1.0 版本之前,API 可能会经历多次重大变更。
贡献
我们正在积极寻找贡献者来帮助我们改进 pyochain!如果您有兴趣参与贡献,请阅读我们的贡献指南以获取有关如何开始的更多信息。
致谢
大多数自定义计算算法的灵感来源于 itertools、cytoolz 和 more-itertools 中的实现。
使用 Pyo3 在 Rust 中编译该库,并提供与 Python 的无缝集成。
使用 Polars 让我意识到从上到下阅读代码是编写 Python 的更好方式,同时也让我接触到了 Rust。