Python的类似Rust的结果类型
项目描述
结果
一个简单的Python 3结果类型,灵感来自Rust的Result,完全类型注解。
安装
最新版本
$ pip install result
最新GitHub main
分支版本
$ pip install git+https://github.com/rustedpy/result
摘要
思想是结果值可以是Ok(value)
或Err(error)
,并且有方法区分两者。Ok
和Err
都是封装任意值的类。Result[T, E]
是typing.Union[Ok[T], Err[E]]
的泛型类型别名。它将改变像这样的代码
def get_user_by_email(email: str) -> Tuple[Optional[User], Optional[str]]:
"""
Return the user instance or an error message.
"""
if not user_exists(email):
return None, 'User does not exist'
if not user_active(email):
return None, 'User is inactive'
user = get_user(email)
return user, None
user, reason = get_user_by_email('ueli@example.com')
if user is None:
raise RuntimeError('Could not fetch user: %s' % reason)
else:
do_something(user)
变成这样
from result import Ok, Err, Result, is_ok, is_err
def get_user_by_email(email: str) -> Result[User, str]:
"""
Return the user instance or an error message.
"""
if not user_exists(email):
return Err('User does not exist')
if not user_active(email):
return Err('User is inactive')
user = get_user(email)
return Ok(user)
user_result = get_user_by_email(email)
if isinstance(user_result, Ok): # or `is_ok(user_result)`
# type(user_result.ok_value) == User
do_something(user_result.ok_value)
else: # or `elif is_err(user_result)`
# type(user_result.err_value) == str
raise RuntimeError('Could not fetch user: %s' % user_result.err_value)
注意,.ok_value
仅存在于Ok
实例上,而.err_value
仅存在于Err
实例上。
如果您使用的是Python 3.10或更高版本,您还可以使用优雅的match
语句。
from result import Result, Ok, Err
def divide(a: int, b: int) -> Result[int, str]:
if b == 0:
return Err("Cannot divide by zero")
return Ok(a // b)
values = [(10, 0), (10, 5)]
for a, b in values:
match divide(a, b):
case Ok(value):
print(f"{a} // {b} == {value}")
case Err(e):
print(e)
并非所有方法(https://doc.rust-lang.net.cn/std/result/enum.Result.html)都已实现,只有那些在Python上下文中有意义的方法已实现。通过使用isinstance
检查Ok
或Err
,当使用MyPy进行类型检查时,您可以得到对包含值的类型安全访问。所有这些都在一个允许更容易处理可能为OK或非OK的值的包中,而不必使用自定义异常。
API
自动生成的API文档也可在./docs/README.md中找到。
创建实例
>>> from result import Ok, Err
>>> res1 = Ok('yay')
>>> res2 = Err('nay')
检查结果是否为Ok
或Err
。您可以使用is_ok
和is_err
类型守卫函数
或isinstance
。这样,您就可以获得可以进行MyPy检查的类型安全访问。如果不需要MyPy的类型安全,可以使用is_ok()
或is_err()
方法
。
>>> res = Ok('yay')
>>> isinstance(res, Ok)
True
>>> is_ok(res)
True
>>> isinstance(res, Err)
False
>>> is_err(res)
False
>>> res.is_ok()
True
>>> res.is_err()
False
您还可以使用OkErr
类型来检查一个对象是否为Ok
或Err
。请注意,此类型仅用于方便,不应用于其他任何目的。使用(Ok, Err)
也行得通。
>>> res1 = Ok('yay')
>>> res2 = Err('nay')
>>> isinstance(res1, OkErr)
True
>>> isinstance(res2, OkErr)
True
>>> isinstance(1, OkErr)
False
>>> isinstance(res1, (Ok, Err))
True
isinstance
的优点是提供了比当前类型守卫更好的类型检查。
res1: Result[int, str] = some_result()
if isinstance(res1, Err):
print("Error...:", res1.err_value) # res1 is narrowed to an Err
return
res1.ok()
res2: Result[int, str] = some_result()
if res1.is_err():
print("Error...:", res2.err_value) # res1 is NOT narrowed to an Err here
return
res1.ok()
有一个提议的PEP 724 – Stricter Type Guards,它可能允许在Python未来版本中,is_ok
和is_err
类型守卫按预期工作。
将Result
转换为值或None
>>> res1 = Ok('yay')
>>> res2 = Err('nay')
>>> res1.ok()
'yay'
>>> res2.ok()
None
将Result
转换为错误或None
>>> res1 = Ok('yay')
>>> res2 = Err('nay')
>>> res1.err()
None
>>> res2.err()
'nay'
直接访问值,无需其他检查
>>> res1 = Ok('yay')
>>> res2 = Err('nay')
>>> res1.ok_value
'yay'
>>> res2.err_value
'nay'
请注意,这是一个属性,您不能将其赋值。结果是不可变的。
当内部值无关紧要时,我们建议使用None
或bool
,但您可以根据认为最适合的任何值来使用。一个Result
的实例(无论是Ok
还是Err
)必须始终包含某些内容。如果您正在寻找可能包含您感兴趣的类型,您可能对maybe感兴趣。
unwrap
方法在Ok
时返回值,unwrap_err
方法在Err
时返回错误值,否则它将引发UnwrapError
。
>>> res1 = Ok('yay')
>>> res2 = Err('nay')
>>> res1.unwrap()
'yay'
>>> res2.unwrap()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "C:\project\result\result.py", line 107, in unwrap
return self.expect("Called `Result.unwrap()` on an `Err` value")
File "C:\project\result\result.py", line 101, in expect
raise UnwrapError(message)
result.result.UnwrapError: Called `Result.unwrap()` on an `Err` value
>>> res1.unwrap_err()
Traceback (most recent call last):
...
>>>res2.unwrap_err()
'nay'
使用expect
和expect_err
可以显示自定义错误消息。
>>> res1 = Ok('yay')
>>> res2 = Err('nay')
>>> res1.expect('not ok')
'yay'
>>> res2.expect('not ok')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "C:\project\result\result.py", line 101, in expect
raise UnwrapError(message)
result.result.UnwrapError: not ok
>>> res1.expect_err('not err')
Traceback (most recent call last):
...
>>> res2.expect_err('not err')
'nay'
可以使用unwrap_or
或unwrap_or_else
返回默认值。
>>> res1 = Ok('yay')
>>> res2 = Err('nay')
>>> res1.unwrap_or('default')
'yay'
>>> res2.unwrap_or('default')
'default'
>>> res1.unwrap_or_else(str.upper)
'yay'
>>> res2.unwrap_or_else(str.upper)
'NAY'
unwrap
方法将引发UnwrapError
。可以通过使用unwrap_or_raise
方法来代替引发自定义异常。
>>> res1 = Ok('yay')
>>> res2 = Err('nay')
>>> res1.unwrap_or_raise(ValueError)
'yay'
>>> res2.unwrap_or_raise(ValueError)
ValueError: nay
可以使用map
、map_or
、map_or_else
和map_err
来映射值和错误。
>>> Ok(1).map(lambda x: x + 1)
Ok(2)
>>> Err('nay').map(lambda x: x + 1)
Err('nay')
>>> Ok(1).map_or(-1, lambda x: x + 1)
2
>>> Err(1).map_or(-1, lambda x: x + 1)
-1
>>> Ok(1).map_or_else(lambda: 3, lambda x: x + 1)
2
>>> Err('nay').map_or_else(lambda: 3, lambda x: x + 1)
3
>>> Ok(1).map_err(lambda x: x + 1)
Ok(1)
>>> Err(1).map_err(lambda x: x + 1)
Err(2)
为了节省内存,Ok
和Err
类都是“槽”的,即它们定义了__slots__
。这意味着将任意属性赋给实例将引发AttributeError
。
as_result
装饰器
可以使用as_result()
装饰器通过指定一个或多个异常类型快速将“正常”函数转换为返回Result
的函数。
@as_result(ValueError, IndexError)
def f(value: int) -> int:
if value == 0:
raise ValueError # becomes Err
elif value == 1:
raise IndexError # becomes Err
elif value == 2:
raise KeyError # raises Exception
else:
return value # becomes Ok
res = f(0) # Err[ValueError()]
res = f(1) # Err[IndexError()]
res = f(2) # raises KeyError
res = f(3) # Ok[3]
可以指定Exception
(甚至BaseException
)来创建一个“捕获所有”的Result
返回类型。这在效果上等同于在大多数场景下不被视为良好实践的try
后跟except Exception
,因此这需要显式选择。
由于as_result
是一个常规装饰器,因此它可以用作包装现有函数(包括来自其他库的函数),尽管语法略有不寻常(没有通常的@
)。
import third_party
x = third_party.do_something(...) # could raise; who knows?
safe_do_something = as_result(Exception)(third_party.do_something)
res = safe_do_something(...) # Ok(...) or Err(...)
if isinstance(res, Ok):
print(res.ok_value)
Do 语法
Do 语法是 and_then()
调用的语法糖。与 Rust 或 Haskell 中的等价语法类似,但语法不同。我们不是写 x <- Ok(1)
,而是写 for x in Ok(1)
。由于语法是基于生成器的,最终结果必须是第一行,而不是最后一行。
final_result: Result[int, str] = do(
Ok(x + y)
for x in Ok(1)
for y in Ok(2)
)
请注意,如果您省略了类型注解,final_result: Result[float, int] = ...
,则类型检查器可能无法推断返回类型。为了避免类型检查器出现错误或警告,您应该在调用 do
函数时添加类型提示。
这类似于 Rust 的 m! 宏
use do_notation::m;
let r = m! {
x <- Some(1);
y <- Some(2);
Some(x + y)
};
请注意,如果您的 do 语句有多个 for 循环,您可以访问之前 for 循环中绑定的标识符。例如
my_result: Result[int, str] = do(
f(x, y, z)
for x in get_x()
for y in calculate_y_from_x(x)
for z in calculate_z_from_x_y(x, y)
)
您可以使用 do()
与延迟值一起使用,如下所示
async def process_data(data) -> Result[int, str]:
res1 = await get_result_1(data)
res2 = await get_result_2(data)
return do(
Ok(x + y)
for x in res1
for y in res2
)
但是,如果您想在表达式中等待某个东西,请使用 do_async()
async def process_data(data) -> Result[int, str]:
return do_async(
Ok(x + y)
for x in await get_result_1(data)
for y in await get_result_2(data)
)
do()
调用的故障排除
TypeError("Got async_generator but expected generator")
有时常规的 do()
可以处理异步值,但这个错误意味着您遇到了它无法处理的场景。您应该在这里使用 do_async()
。
贡献
以下步骤应该在安装了 Python 和 make
的任何基于 Unix 的系统(Linux、macOS 等)上工作。在 Windows 上,您需要参考下面的 Python 文档和 Makefile
,以获取在 Windows 使用的非 Unix Shell 中运行命令的说明。
- 设置并激活虚拟环境。有关虚拟环境和设置的更多信息,请参阅 Python 文档。
- 运行
make install
以安装依赖项 - 切换到一个新的 git 分支并做出您的更改
- 测试您的更改
运行
make test
运行
make lint
- 您还可以启动一个 Python REPL 并导入
result
- 更新文档
- 编辑任何相关的 docstrings,markdown 文件
- 运行
make docs
- 在 变更日志 中添加一个条目
- 提交所有更改并进行新的 Git 提交。
常见问题解答(FAQ)
- 为什么我在 MyPy 中得到 "无法推断类型参数" 错误?
在 MyPy 中存在一个 错误,在某些情况下可能会被触发。在某些情况下,使用 if isinstance(res, Ok)
而不是 if res.is_ok()
可以解决问题。否则,可以使用 这些解决方案之一 来帮助解决问题。
相关项目
- dry-python/returns: 让您的函数返回有意义的、类型化且安全的对象!
- alexandermalyga/poltergeist: 考虑类型安全性的 Rust 风格的 Python 错误处理。
许可协议
MIT 许可证
项目详细信息
下载文件
下载您平台所需的文件。如果您不确定选择哪个,请了解更多关于安装包的信息。