我有一个计算
Result
的函数,这个计算可以是 success
full 也可以不是。如果成功,也会返回一些总结计算结果的data
。如果不成功,该数据将为None
。现在的问题是,即使我验证了计算的状态 (success
),类型检查器 (mypy
) 也无法推断 success
和 data
之间的耦合。这可以通过以下代码进行总结:
from dataclasses import dataclass
from typing import Optional
@dataclass
class Result:
success: bool
data: Optional[int] # This is not None if `success` is True.
def compute(inputs: str) -> Result:
if inputs.startswith('!'): # Oops, some condition that prevents the computation.
return Result(success=False, data=None)
return Result(success=True, data=len(inputs))
def check(inputs: str) -> bool:
return (result := compute(inputs)).success and result.data > 2
assert check('123')
assert not check('12')
assert not check('!123')
针对此代码运行
mypy
会出现以下错误:
test.py:18: error: Unsupported operand types for < ("int" and "None") [operator]
test.py:18: note: Left operand is of type "Optional[int]"
我考虑了以下解决方案,但我对其中任何一个都不满意。所以我想知道是否有更好的方法来解决这个问题。
typing.cast
函数
check
可以修改为使用 cast(int, result.data)
来强制 success
和 data
之间的逻辑关系。然而,不得不求助于 cast
感觉像是代码有问题的迹象(至少在这种情况下)。另外,每次在代码中使用此关系时,我都必须使用 cast
。最好在一处解决。
result.data is not None
在上面的例子中,
success
和data
之间的关系非常简单:success == data is not None
。因此,我可以完全删除属性 success
,而是检查 result.data is not None
。
def check(inputs: str) -> bool:
return (result := compute(inputs)).data is not None and result.data > 2
虽然这有效,但实际用例更加复杂,并且存在各种数据字段,例如
data_x
、data_y
和 data_z
。在这种情况下,success == all(d is not None for d in [data_x, data_y, data_z])
。使用它作为检查过于冗长,因此我将其重构为 property
类的 Result
。对于上面的例子,这将是:
@dataclass
class Result:
data: Optional[int]
@property
def success(self) -> bool:
return self.data is not None
但是,当
is not None
检查移入属性时,当 mypy
是 result.data
时,None
无法再推断 result.success
确实不是 True
。
您最初的问题与另一个问题非常接近,但总体问题不仅仅是这个,所以我会回答。
这是几个选项中的一个。在一般情况下您有
success == all(d is not None for d in [data_x, data_y, data_z])
的事实表明,您真正想要的是一个简单的
Result
类型,并组合它们。在 Haskell/Rust 等中有一个非常完善的模式,尽管它通常被称为
Maybe
或
Option
。在 Python 中,这看起来像
@dataclass
class Success[T]:
data: T
class Fail:
pass
Result[T] = Success[T] | Fail
你会像这样使用它
def compute(inputs: str) -> Result[int]:
if inputs.startswith('!'): # Oops, some condition that prevents the computation.
return Fail()
return Success(len(inputs))
现在我不清楚check
在这里扮演什么角色,因为这取决于您的需求。最简单的方法是
def check(inputs: str) -> bool:
match compute(inputs):
case Success(x):
return result.data > 2
case Fail():
return False
但是你可能会发现类似的功能
def is_success[T](r: Result[T]) -> bool:
return isinstance(r, Success)
def map[T, U](result: Result[T]) -> Result[U]:
match result:
case Success(x):
return x
case Fail:
return Fail()
更普遍有用,在这种情况下你可能会这样做
def check(inputs: str) -> bool:
return is_success(map(compute(inputs), lambda data: result.data > 2))
这不太漂亮,部分原因是我真的不知道 check
的用途。当您有多个值时,您可以使用
组合器来组合多个函数,例如
def map2[T, U, V](r0: Result[T], r1: Result[U], f: Callable[[T, U], V]) -> Result[V]:
match (r0, r1):
case (Success(x0), Success(x1)):
return f(x0, x1)
case _:
return Fail()
@dataclass
class TwoThings:
data0: int
data1: int
hopefully_two_things: Result[TwoThings] = map2(compute("foo"), compute("bar"), TwoThings)