跳转到主要内容

Python的类似Rust的结果类型

项目描述

结果

GitHub Workflow Status (branch) Coverage

Python 3的简单Result类型,受Rust 启发,完全类型注解。

安装

最新发布版本

$ pip install result

最新GitHub master分支版本

$ 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进行检查。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类型来检查一个对象是否为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

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,但您可以使用任何您认为最适合的值。一个ResultOkErr)的实例必须始终包含某些内容。如果您正在寻找可能包含值的类型,您可能对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()与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来获取命令。

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

常见问题解答

  • 为什么我使用MyPy时会遇到“无法推断类型参数”的错误?

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

许可协议

MIT许可证

项目详情


下载文件

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

源分布

result-test-publish-version-0.17.0.dev1.tar.gz (19.1 kB 查看哈希值)

上传时间

构建分布

由以下机构支持

AWS AWS 云计算和安全赞助商 Datadog Datadog 监控 Fastly Fastly CDN Google Google 下载分析 Microsoft Microsoft PSF 赞助商 Pingdom Pingdom 监控 Sentry Sentry 错误记录 StatusPage StatusPage 状态页面