假设我们有一个 Python 数据类
MyData
,它被设计用来存储和执行一些数据的操作。
该数据类由 3 个元素组成,a
、b
和 c
:
a
为必填项,必须提供。b
和 c
是可选的,但至少必须指定其中一个。
如果 b
没有给出,我们可以从 a
和 c
在 __post_init__
中计算它。
c
也一样。这些计算并不便宜,并且在代码其他地方的函数中定义。对于我们人类来说,如果你提供
a
和b
或c
中的至少一个,我们可以假设,在初始化之后,所有属性都是float
类型,所以我们可以传递MyData
的实例
考虑到这个假设。
然而,如果你尝试这样做并运行像 mypy 这样的静态类型检查器,你会得到错误——因为它们仍然考虑它们可能未定义的情况。
如何实现这个数据类,以便像 mypy 这样的类型检查器不会抱怨?
from dataclasses import dataclass
from typing import cast
from time import sleep
@dataclass
class MyData:
a: float
b: float | None = None
c: float | None = None
def __post_init__(self) -> None:
self._complete_attributes()
def _complete_attributes(self) -> None:
"""Checks for missing attributes and computes them if possible."""
if self.b is None and self.c is None:
# Both b and c are missing, oops!
raise ValueError("Either b or c needs to be defined!")
elif self.b is None:
# b is missing but c is there
# typing.cast does nothing in runtime but convinces mypy that c is not None
# I don't like it being there but it suppresses a mypy error.
# Is there a better way?
self.c = cast(float, self.c)
self.b = compute_b(self.a, self.c)
elif self.c is None:
# c is missing but b is there
self.b = cast(float, self.b)
self.c = compute_c(self.a, self.b)
def compute_b(a: float, c: float) -> float:
sleep(10) # Simulate number-crunching
return a + c
def compute_c(a: float, b: float) -> float:
sleep(10) # Gotta think hard about this one
return b - a
这似乎没有问题。 但是,如果您尝试使用
b
或 c
进行任何操作,例如:
def do_stuff(data: MyData) -> float:
return data.a * data.b
mypy会报错:
error: Unsupported operand types for * ("float" and "None") [operator]
note: Right operand is of type "Optional[float]"
我们如何获得所需的行为并让静态类型检查器开心?
因此,您可以组合使用
dataclasses.InitVar
(表示该值仅用作__init__
的参数)和field(init=False)
(指定字段不应为__init__
产生相应的参数), 但这真的很乱:
import dataclasses
@dataclasses.dataclass
class MyData:
a: float
b: float = dataclasses.field(init=False)
c: float = dataclasses.field(init=False)
b_arg: dataclasses.InitVar[float | None] = None
c_arg: dataclasses.InitVar[float | None] = None
def __post_init__(self, b_arg, c_arg) -> None:
self._complete_attributes(b_arg, c_arg)
def _complete_attributes(self, b_arg, c_arg) -> None:
"""Checks for missing attributes and computes them if possible."""
if b_arg is None and c_arg is None:
# Both b and c are missing, oops!
raise ValueError("Either b or c needs to be defined!")
elif b_arg is None:
# b is missing but c is there
self.b = compute_b(self.a, c_arg)
self.c = c_arg
elif c_arg is None:
# c is missing but b is there
self.b = b_arg
self.c = compute_c(self.a, b_arg)
def compute_b(a: float, c: float) -> float:
return a + c
def compute_c(a: float, b: float) -> float:
return b - a
坦率地说,我会在这里放弃
dataclasses.dataclass
,这意味着使您的代码更具可读性并避免样板,但上面的内容非常丑陋和混乱。在没有代码生成器的情况下简单地硬着头皮写出类定义会好得多,IMO。