如何使用可选属性“完成”部分填充的数据类并通过类型检查?

问题描述 投票:0回答:1

问题

假设我们有一个 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]"

我们如何获得所需的行为并让静态类型检查器开心?

python mypy python-typing python-dataclasses
1个回答
0
投票

因此,您可以组合使用

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。

© www.soinside.com 2019 - 2024. All rights reserved.