跳转到主要内容

Python的类似Rust的结果类型

项目描述

结果

GitHub Workflow Status (branch) Coverage

一个简单的Python 3结果类型,灵感来自Rust的Result,完全类型注解。

安装

最新版本

$ pip install result

最新GitHub main分支版本

$ pip install git+https://github.com/rustedpy/result

摘要

思想是结果值可以是Ok(value)Err(error),并且有方法区分两者。OkErr都是封装任意值的类。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检查OkErr,当使用MyPy进行类型检查时,您可以得到对包含值的类型安全访问。所有这些都在一个允许更容易处理可能为OK或非OK的值的包中,而不必使用自定义异常。

API

自动生成的API文档也可在./docs/README.md中找到。

创建实例

>>> from result import Ok, Err
>>> res1 = Ok('yay')
>>> res2 = Err('nay')

检查结果是否为OkErr。您可以使用is_okis_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类型来检查一个对象是否为OkErr。请注意,此类型仅用于方便,不应用于其他任何目的。使用(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_okis_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'

请注意,这是一个属性,您不能将其赋值。结果是不可变的。

当内部值无关紧要时,我们建议使用Nonebool,但您可以根据认为最适合的任何值来使用。一个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'

使用expectexpect_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_orunwrap_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

可以使用mapmap_ormap_or_elsemap_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)

为了节省内存,OkErr类都是“槽”的,即它们定义了__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 中运行命令的说明。

  1. 设置并激活虚拟环境。有关虚拟环境和设置的更多信息,请参阅 Python 文档
  2. 运行 make install 以安装依赖项
  3. 切换到一个新的 git 分支并做出您的更改
  4. 测试您的更改
  • 运行 make test
  • 运行 make lint
  • 您还可以启动一个 Python REPL 并导入 result
  1. 更新文档
  • 编辑任何相关的 docstrings,markdown 文件
  • 运行 make docs
  1. 变更日志 中添加一个条目
  2. 提交所有更改并进行新的 Git 提交。

常见问题解答(FAQ)

  • 为什么我在 MyPy 中得到 "无法推断类型参数" 错误?

在 MyPy 中存在一个 错误,在某些情况下可能会被触发。在某些情况下,使用 if isinstance(res, Ok) 而不是 if res.is_ok() 可以解决问题。否则,可以使用 这些解决方案之一 来帮助解决问题。

相关项目

许可协议

MIT 许可证

项目详细信息


下载文件

下载您平台所需的文件。如果您不确定选择哪个,请了解更多关于安装包的信息。

源分发

result-0.17.0.tar.gz (20.2 kB 查看哈希值)

上传时间

构建分发

result-0.17.0-py3-none-any.whl (11.7 kB 查看哈希值)

上传时间 Python 3

支持者