1

想象一下,我有一个对象,它是一个类的实例,如下所示:

@dataclass
class Foo:
    bar: int
    baz: str

我使用dataclasses是为了方便,但在这个问题的上下文中,没有要求类是dataclass.

通常,如果我想解包这样一个对象的属性,我必须实现__iter__,例如如下:

class Foo:
    ...
    def __iter__(self) -> Iterator[Any]:
        return iter(dataclasses.astuple(self))

bar, baz = Foo(1, "qux")

但是,从像pyright这样的静态类型检查器的角度来看,我现在已经丢失了 and 的任何类型信息barbaz它只能推断是 type Anyiter我可以通过手动创建元组参数来稍微改进:

    def __iter__(self) -> Iterator[Union[str, int]]:
        return iter((self.bar, self.baz))

但是我仍然没有特定的类型barand baz。我可以注释bar然后直接baz使用dataclasses.astuple如下:

bar: str
baz: int
bar, baz = dataclasses.astuple(Foo(1, "qux"))

但这需要可读性较差的多级列表理解,例如

bars: list[int] = [
    bar for bar, _ in [dataclasses.astuple(foo) for foo in [(Foo(1, "qux"))]]
]

也把我绑在dataclasses.

显然,这一切都不是不可克服的。如果我想使用类型检查器,我不能使用解包语法,但如果有一种干净的方法可以做到这一点,我真的很想这样做。

如果当前无法使用通用方法,则可以接受特定于dataclasses或更好的答案。attrs

4

1 回答 1

0

正如 juanpa.arrivillaga 所指出的,赋值语句文档表明,如果赋值语句的左侧是一个或多个目标的逗号分隔列表,

该对象必须是具有与目标列表中的目标相同数量的项目的可迭代对象,并且项目从左到右分配给相应的目标。

因此,如果要解包一个裸对象,就必须实现,当它包含多个属性类型时,它__iter__的返回类型总是为Iterator[Union[...]]or 。Iterator[SufficientlyGenericSubsumingType]因此,静态类型检查器不能有效地推断解包变量的特定类型。

据推测,当 atuple位于赋值的右侧时,即使语言规范表明它将被视为可迭代,静态类型检查器仍然可以有效地推断其组成部分的类型。

因此,正如 juanpa.arrivillaga 所指出的那样,如果必须解包属性,那么astuple发出tuple[...]类型的定制方法可能是最好的方法,即使它不能避免问题中提到的多级列表理解的陷阱。就问题而言,我们现在可以:

@dataclass
class Foo:
    bar: int
    baz: str

    def astuple(self) -> tuple[int, str]:
        return self.bar, self.baz


bar, baz = Foo(1, "qux").astuple()
bars = [bar for bar, _ in [foo.astuple() for foo in [(Foo(1, "qux"))]]]

没有任何明确的目标注释,只要我们愿意编写额外的类样板。

dataclasses's 和attrs'函数的返回值都不astuple比 更好tuple[Any, ...],因此如果我们选择使用这些目标,仍必须单独注释这些目标。

但是,对于列表理解,这些是否比

bars = [foo.bar for foo in [Foo(1, "qux")]]

? 在大多数情况下,可能不会。

最后一点,attrs 为什么不呢?页面提到,关于“为什么不命名元组?”,那

由于它们是元组的子类,因此命名元组具有长度并且是可迭代和可索引的。这不是您对课程的期望,并且可能会掩盖细微的错字错误。

可迭代性还意味着很容易意外解包命名元组,这会导致难以发现的错误。

我不确定我是否完全同意其中任何一点,但对于其他想要走这条路的人来说,需要考虑一些事情。

于 2021-11-10T05:15:08.387 回答