Python的类似Rust的结果类型
项目描述
结果
Python 3的简单Result类型,受Rust 启发,完全类型注解。
安装
最新发布版本
$ pip install result
最新GitHub master
分支版本
$ 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进行检查。is_ok()
或is_err()
的方法在您不需要MyPy的类型安全时可以使用。
>>> 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
将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()
与awaited值如下
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
)
但是,如果您想在表达式中await某个值,请使用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文档,并参考Windows上使用的非Unix shell中的Makefile
来获取命令。
- 设置并激活一个虚拟环境。有关虚拟环境和设置的更多信息,请参阅Python文档。
- 运行
make install
来安装依赖项 - 切换到一个新的git分支并进行修改
- 测试你的更改
运行
make test
运行
make lint
- 你还可以启动一个Python交互式解释器并导入
result
- 更新文档
- 编辑任何相关的docstrings或markdown文件
- 运行
make docs
- 在变更日志中添加条目
- 提交你的所有更改并创建一个新的PR。
常见问题解答
- 为什么我使用MyPy时会遇到“无法推断类型参数”的错误?
MyPy中存在一个错误,在某些情况下可能会被触发。在某些情况下,使用if isinstance(res, Ok)
而不是if res.is_ok()
可能会有帮助。否则,使用这些解决方案之一可能会有帮助。
许可协议
MIT许可证
项目详情
哈希值 for result-test-publish-version-0.17.0.dev1.tar.gz
算法 | 哈希摘要 | |
---|---|---|
SHA256 | 02cce08291832af58b78db88b66067fdee917ad828efe62f1cdf1376f150f358 |
|
MD5 | e508ad7832d8a74eee247097753db50b |
|
BLAKE2b-256 | 01d97c83a9922717263dfc11af60d6a4cc00a196ffc7475c80ab56a921ecd452 |
哈希值 for result_test_publish_version-0.17.0.dev1-py3-none-any.whl
算法 | 哈希摘要 | |
---|---|---|
SHA256 | 2bfd84644c3113a57c3a1603a58a1ba40522dee8f90746097461d3015e9a3639 |
|
MD5 | d768137a7e2f5a7b9b667ffc3ae910da |
|
BLAKE2b-256 | 1bc4bcda2646130d4fe42440ec3d127717ea860f92359a2f005394aa0e5e71d5 |