在 Python 类型中声明元组的长度

Declaring length of tuples in Python typing

我想知道是否要提交给元组[float, ...],即使我知道元组的长度。

我有一个 Point 和一个 Rect class,Rect class 中有一个 属性 aspoints 是 return 的一个元组左上角和右下角,作为二元组。

迭代器的类型为 Iterator[float],我知道它会给我两个浮点数。我希望 属性 的 return 值是 Tuple[Tuple[float, float], Tuple[float, float]] 因为我知道迭代器会为每个点提供两个浮点数。

我应该提交,只说它会return一个Tuple[Tuple[float, ...], Tuple[float, ...]],在文档中留下他们的长度的评论,或者有更好的解决方案吗?

这是代码。

from dataclasses import dataclass
from typing import Iterator, Tuple

@dataclass
class Point:
    x: float
    y: float

    def __iter__(self) -> Iterator[float]:
        return iter((self.x, self.y))

@dataclass
class Rect:
    x: float
    y: float
    width: float
    height: float

    @property
    def tl(self) -> Point:
        return Point(self.x, self.y)

    @property
    def br(self) -> Point:
        return Point(self.x + self.width, self.y + self.height)

    @property
    def aspoints(self) -> Tuple[Tuple[float, float], Tuple[float, float]]:
        return tuple(self.tl), tuple(self.br)

问题出现在Rect.aspoints。从 MyPy 我得到以下错误:

error: Incompatible return value type (got "Tuple[Tuple[float, ...], Tuple[float, ...]]", expected "Tuple[Tuple[float, float], Tuple[float, float]]")

问题似乎是您对迭代器将 return 的元素数量的了解无法编码为静态类型。

我不确定重载 __iter__ 以迭代固定数量的元素是最好的方法,因为您基本上是将它用作允许将对象转换为元组的技巧。

也许向点数据 class 添加类似 "to_tuple()" 方法的方法更有意义?您可以在那里声明类型...

编辑:或者,我猜你可以解构迭代器的输出,但你仍然没有节省很多代码:

a, b = self.tl
c, d = self.br
return (a, b), (c, d)

您可以扩展 aspoints 函数以正确转换字段类型:

def aspoints(self) -> Tuple[Tuple[float, float], Tuple[float, float]]:
    left = cast(Tuple[float, float], tuple(self.tl))
    right = cast(Tuple[float, float], tuple(self.br))
    return left, right

您也可以将所有内容都放在一行中,但可读性会受到很大影响。 cast 函数在运行时不会做任何事情,它只是作为一种明确的方式告诉 mypy(或其他一些静态类型检查器)你对你的类型的了解比基本类型工具所能表达的更多。

绝对不要__iter__ 的 return 类型更改为任何不是迭代器的类型,那样会非常奇怪和令人困惑。

另一种方法:您真的不想迭代 Point;你只是想要一种方法来获得 float 的元组,你只是让你的点可迭代,这样你就可以将它直接传递给 tuple。 (考虑一下:你会写出看起来像 for coord in Point(3, 5): ... 的代码吗?)

而不是定义 __iter__,而是定义一个函数来执行您真正想要的操作:returns 一对 floats。

from typing import Tuple


PairOfFloats = Tuple[float,float]


@dataclass
class Point:
    x: float
    y: float

    def as_cartesian_pair(self) -> PairOfFloats:
        return (self.x, self.y)


@dataclass
class Rect:
    x: float
    y: float
    width: float
    height: float

    @property
    def tl(self) -> Point:
        return Point(self.x, self.y)

    @property
    def br(self) -> Point:
        return Point(self.x + self.width, self.y + self.height)

    @property
    def aspoints(self) -> Tuple[PairOfFloats, PairOfFloats]:
        return self.tl.as_cartesian_pair(), self.br.as_cartesian_pair()

为了支持这种方法,它还允许您编写一个 附加 方法,该方法也是 returns 一对浮点数,但具有不同的语义:

def as_polar_pair(self) -> PairOfFloats:
    return cmath.polar(complex(self.x, self.y))

关于解包,定义Point.__getitem__而不是Point.__iter__就足够了:

def __getitem__(self, i) -> float:
    if i == 0:
        return self.x
    elif i == 1:
        return self.y
    else:
        raise IndexError


>>> p = Point(3,5)
>>> p[0], p[1]
(3, 5)
>>> x, y = p
>>> x
3
>>> y
5

鉴于需求,似乎用typing.NamedTuple作为Point的基础class是最合适的。这将大大简化事情,因为正在使用 none 的 dataclasses.dataclass 功能,而且 Point 在给定的示例中似乎也不需要可变性。

from typing import Tuple, NamedTuple
from dataclasses import dataclass

class Point(NamedTuple):
    x: float
    y: float

@dataclass
class Rect:
    ...
    @property
    def aspoints(self) -> Tuple[Point, Point]:
        return (self.tl, self.br)

尽管如此,似乎 Rect 也可能更合适地从 typing.NamedTuple 继承,原因与 Point.

已经提到的相同