A modern, lightweight Rust-style Result type for Python 3.10+ pattern matching.
Project description
NoNone
拒绝隐式
None,拥抱现代 Python 的 Rust 风格 Result 类型。
NoNone 是一个轻量、纯粹且类型安全的 Python 库。它引入了 Rust 语言中著名的 Result(Ok / Err)模式,并专为 Python 3.10+ 的 match-case(结构化模式匹配)语法量身定制。
为什么选择 NoNone?告别 None 和嵌套判断的痛苦
在传统的 Python 开发中,我们经常面临以下痛点:
痛点 1:隐式 None 导致的运行时错误
❌ 传统写法 - 危险!
def find_user(user_id: int) -> User | None:
# ... 数据库查询逻辑
return None if not found else user
user = find_user(123)
print(user.name) # 💥 如果 user 是 None,这里会抛出 AttributeError!
✅ NoNone 写法 - 类型安全
from nonone import Result, ok, err, Ok, Err
def find_user_safe(user_id: int) -> Result[dict, str]:
# 模拟数据库查询
users = {123: {"id": 123, "name": "Alice"}}
if user_id in users:
return ok(users[user_id])
return err("用户不存在")
result = find_user_safe(123)
# 编译器和 IDE 会强制你处理两种情况,无法忽略错误
match result:
case Ok(user):
print(user["name"]) # ✅ 安全,user 一定存在
case Err(msg):
print(f"错误: {msg}")
痛点 2:繁琐的嵌套 None 判断
❌ 传统写法 - 嵌套地狱
user = find_user(123)
if user is not None:
profile = get_profile(user.id)
if profile is not None:
address = get_address(profile.address_id)
if address is not None:
print(f"用户地址: {address.street}")
else:
print("地址不存在")
else:
print("用户资料不存在")
else:
print("用户不存在")
✅ NoNone 写法 - 扁平化链式调用
from nonone import ok, err, Result
# 定义安全的函数(完整实现见下方"完整示例")
def find_user_safe(user_id: int) -> Result[dict, str]:
users = {123: {"id": 123, "name": "Alice"}}
return ok(users[user_id]) if user_id in users else err("用户不存在")
def get_profile_safe(user: dict) -> Result[dict, str]:
profiles = {123: {"user_id": 123, "address_id": 456}}
return ok(profiles[user["id"]]) if user["id"] in profiles else err("资料不存在")
def get_address_safe(profile: dict) -> Result[dict, str]:
addresses = {456: {"profile_id": 456, "street": "长安街"}}
return ok(addresses[profile["address_id"]]) if profile["address_id"] in addresses else err("地址不存在")
def format_address(address: dict) -> str:
return f"用户地址: {address['street']}"
# 使用链式调用,完全避免嵌套
result = (
find_user_safe(123) # 第1步: 查找用户
.and_then(get_profile_safe) # 第2步: 获取资料
.and_then(get_address_safe) # 第3步: 获取地址
.map(format_address) # 第4步: 格式化输出
)
match result:
case Ok(message):
print(message) # ✅ 输出: 用户地址: 长安街
case Err(msg):
print(f"处理失败: {msg}") # 任何一步出错都会到这里
💡 理解链式调用中的关键方法:
-
.and_then(func)- 当前一步成功时,执行下一个返回Result的操作- 如果前一步是
Err,则跳过后续所有步骤 - 用于串联多个可能失败的操作
- 如果前一步是
-
.map(func)- 对成功的值进行转换(不改变 Result 结构)- 只在
Ok时执行,将Ok(value)转换为Ok(func(value)) - 用于最后的格式化、计算等纯函数操作
- 只在
对比理解:
# 假设 get_address_safe 返回 Ok(Address(street="长安街"))
# 使用 .map() 转换结果
result = get_address_safe(123).map(lambda addr: addr.street)
# 结果: Ok("长安街") ← 仍然是 Ok,但内部值从 Address 变成了 str
# 如果不使用 .map()
result = get_address_safe(123)
# 结果: Ok(Address(street="长安街")) ← 内部还是 Address 对象
痛点 3:异常处理的复杂性与遗漏风险
❌ 传统写法 - try-except 容易遗漏
try:
user = find_user(123)
profile = get_profile(user.id) # 如果 user 是 None,这里也会出错!
address = get_address(profile.address_id)
print(f"用户地址: {address.street}")
except (AttributeError, ValueError, DatabaseError) as e:
print(f"处理失败: {e}")
# ⚠️ 问题:可能忘记捕获某些异常,或者异常类型不匹配
✅ NoNone 写法 - 统一错误处理
from nonone import catch, try_catch
# 方式 A: 使用装饰器 - 一键转换现有函数
@catch
def dangerous_divide(a: float, b: float) -> float:
return a / b # 可能抛出 ZeroDivisionError
result = dangerous_divide(10, 0)
# 自动包装为: Err(ZeroDivisionError(...))
# 方式 B: 临时调用 - 无需修改原函数定义
import requests
def fetch_data(url: str) -> dict:
response = requests.get(url)
response.raise_for_status()
return response.json()
# 直接包装调用,立即获得 Result
result = try_catch(fetch_data, "https://api.example.com/data")
match result:
case Ok(data):
print(f"获取成功: {data}")
case Err(e):
print(f"请求失败: {type(e).__name__}: {e}")
# ✅ 所有异常都被统一捕获并转换为 Err,不会遗漏
NoNone 的核心优势
- ✅ 编译时类型安全:基于双泛型 Result[T, E] 设计,静态类型检查器能提前发现潜在错误
- ✅ 强制错误处理:无法忽略错误情况,必须显式处理所有分支
- ✅ 消除 None 隐患:再也不用担心
AttributeError: 'NoneType' object has no attribute... - ✅ 扁平化代码结构:通过
.map()、.and_then()等链式调用将多层嵌套变为线性流程 - ✅ 统一的错误模型:所有错误都是
Err,无需记忆各种异常类型 - ✅ 无缝迁移:使用
@catch或try_catch零成本改造现有代码 - ✅ 现代语法支持:专为 Python 3.10+ 的
match-case结构化模式匹配量身定制 - ✅ 极简无包袱:零第三方依赖,基于
slots=True的数据类构建,极致轻量且高效
安装
使用 uv(推荐)或 pip 安装 NoNone:
uv pip install nonone
# 或者
pip install nonone
快速上手
💡 重要提示:大写 vs 小写
NoNone 提供了两套 API,用途不同但很简单:
| 场景 | 使用 | 示例 |
|---|---|---|
| 构造实例 | 小写 ok() / err() |
return ok(42) |
| 模式匹配 | 大写 Ok / Err |
case Ok(value): |
| 类型注解 | 大写 Result |
-> Result[int, str] |
简单记忆:🏗️ 建造时用小写的(函数),🔍 检查时用大的(类)
完整示例:从安装到运行
让我们通过一个完整的例子来体验 NoNone:
# example.py - 完整的用户地址查询示例
from nonone import ok, err, Result, Ok, Err
# 1. 定义数据模型(简化版,实际项目中使用 dataclass)
User = dict
Profile = dict
Address = dict
# 2. 定义安全的业务函数
def find_user(user_id: int) -> Result[User, str]:
"""查找用户"""
users = {123: {"id": 123, "name": "Alice"}}
return ok(users[user_id]) if user_id in users else err("用户不存在")
def get_profile(user: User) -> Result[Profile, str]:
"""获取用户资料"""
profiles = {123: {"user_id": 123, "address_id": 456}}
return ok(profiles[user["id"]]) if user["id"] in profiles else err("资料不存在")
def get_address(profile: Profile) -> Result[Address, str]:
"""获取地址信息"""
addresses = {456: {"profile_id": 456, "street": "长安街"}}
return ok(addresses[profile["address_id"]]) if profile["address_id"] in addresses else err("地址不存在")
# 3. 使用链式调用组合业务逻辑
def get_user_address(user_id: int) -> Result[str, str]:
return (
find_user(user_id)
.and_then(get_profile)
.and_then(get_address)
.map(lambda addr: f"用户 {user_id} 的地址是: {addr['street']}")
)
# 4. 处理结果
if __name__ == "__main__":
result = get_user_address(123)
match result:
case Ok(message):
print(f"✅ {message}")
case Err(error):
print(f"❌ {error}")
运行结果:
$ python example.py
✅ 用户 123 的地址是: 长安街
进阶用法:强大的 match-case
NoNone 基于 dataclass 构建,可以充分利用 Python 模式匹配的强大功能。
🎯 使用 Guard(条件过滤)
在 case 中添加 if 条件,实现更精细的控制:
from nonone import ok, Ok, Err
def divide(a: float, b: float):
if b == 0:
return err("除数不能为零")
return ok(a / b)
result = divide(100, 2)
match result:
case Ok(val) if val > 10:
print(f"得到一个较大的数: {val}") # ✅ 输出: 得到一个较大的数: 50.0
case Ok(val):
print(f"得到一个较小的数: {val}")
case Err(msg):
print(f"计算失败: {msg}")
📦 嵌套对象解构
如果 Ok 里面包裹的是 dataclass,可以直接在 case 中解构其属性:
from dataclasses import dataclass
from nonone import ok, Ok, Err
@dataclass
class User:
id: int
name: str
role: str
def get_user() -> Result[User, str]:
return ok(User(id=1, name="Alice", role="admin"))
match get_user():
# 直接解构内部对象的特定属性
case Ok(User(role="admin", name=name)):
print(f"管理员登录: {name}") # ✅ 输出: 管理员登录: Alice
case Ok(User(name=name)):
print(f"普通用户登录: {name}")
case Err(error):
print(f"登录失败: {error}")
这种深度解构让你可以在一行代码中完成类型检查 + 属性提取 + 条件判断,非常优雅!
API 使用指南
根据你的需求选择合适的 API:
🏗️ 构造 Result
from nonone import ok, err
ok(42) # 成功结果
err("出错了") # 失败结果
🔍 检查结果状态
result.is_ok() # True/False
result.is_err() # True/False
📦 提取值(谨慎使用)
result.unwrap() # 成功返回值,失败抛出 UnwrapError
result.unwrap_or(0) # 成功返回值,失败返回默认值 0
result.expect("错误提示") # 成功返回值,失败抛出带自定义消息的异常
⛓️ 链式操作(推荐)
# map: 转换成功的值
ok(5).map(lambda x: x * 2) # Ok(10)
# and_then: 串联多个可能失败的操作
ok(5).and_then(lambda x: ok(x * 2)) # Ok(10)
# or_else: 失败时尝试恢复
err("错").or_else(lambda e: ok(0)) # Ok(0)
🎯 模式匹配(最安全)
from nonone import Ok, Err
match result:
case Ok(value):
print(f"成功: {value}")
case Err(error):
print(f"失败: {error}")
🛡️ 捕获异常
from nonone import catch, try_catch
# 装饰器方式
@catch
def risky_func():
return 1 / 0
# 函数方式
result = try_catch(risky_func)
许可协议
本项目基于 MIT License 开源。
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file nonone-0.1.5.tar.gz.
File metadata
- Download URL: nonone-0.1.5.tar.gz
- Upload date:
- Size: 6.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b6c90814e1481c64a875bc91dffe1bdc8825899efacad42ee7fced6a9e60feef
|
|
| MD5 |
d770e437559158336f32a5078c6aa270
|
|
| BLAKE2b-256 |
0e70bcbfedaf0ca58025db21badb8e64dba9ac86d4af3d3e9a3a52d274a1f9c3
|
Provenance
The following attestation bundles were made for nonone-0.1.5.tar.gz:
Publisher:
publish_to_pypi.yml on DBinK/nonone
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
nonone-0.1.5.tar.gz -
Subject digest:
b6c90814e1481c64a875bc91dffe1bdc8825899efacad42ee7fced6a9e60feef - Sigstore transparency entry: 1396092205
- Sigstore integration time:
-
Permalink:
DBinK/nonone@03145013ac43e720e967c03b675d30d09c7492fe -
Branch / Tag:
refs/tags/v0.1.5 - Owner: https://github.com/DBinK
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish_to_pypi.yml@03145013ac43e720e967c03b675d30d09c7492fe -
Trigger Event:
push
-
Statement type:
File details
Details for the file nonone-0.1.5-py3-none-any.whl.
File metadata
- Download URL: nonone-0.1.5-py3-none-any.whl
- Upload date:
- Size: 7.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8129c6e279b0529d8223a70cec65cade985f9eabd06c8962ec4264fae37e9dbf
|
|
| MD5 |
6ad8b7c9be70872659452272758ab993
|
|
| BLAKE2b-256 |
8abdaa3b309cc71977368467780e0e7a83397f0afd19c7e8bb98166e58ad1b81
|
Provenance
The following attestation bundles were made for nonone-0.1.5-py3-none-any.whl:
Publisher:
publish_to_pypi.yml on DBinK/nonone
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
nonone-0.1.5-py3-none-any.whl -
Subject digest:
8129c6e279b0529d8223a70cec65cade985f9eabd06c8962ec4264fae37e9dbf - Sigstore transparency entry: 1396092210
- Sigstore integration time:
-
Permalink:
DBinK/nonone@03145013ac43e720e967c03b675d30d09c7492fe -
Branch / Tag:
refs/tags/v0.1.5 - Owner: https://github.com/DBinK
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish_to_pypi.yml@03145013ac43e720e967c03b675d30d09c7492fe -
Trigger Event:
push
-
Statement type: