【问题标题】:Is it possible to maintain type information when unpacking object attributes?解包对象属性时是否可以维护类型信息?
【发布时间】:2021-11-09 22:56:39
【问题描述】:

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

@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 这样的静态类型检查器的角度来看,我现在丢失了barbaz 的任何类型信息,它只能推断出类型为Any。我可以通过手动创建 iter 元组参数来稍微改进:

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

但我仍然没有barbaz 的特定类型。我可以注释barbaz然后直接使用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联系起来。

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

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

【问题讨论】:

  • 另一种方法是编写一个定制方法astuple,它返回一个您正确注释的Tuple[x,y],然后使用bar, baz = Foo.astuple()
  • @juanpa.arrivillaga 这比dataclasses.astuple 或其他等价物略好,是的,但我希望有一个我不知道的类似dunder方法的技巧来使解包语法起作用裸露的物体。这可能是目前不可能的,因为解包必须使用 __iter__ 并且当有多个包含类型时,__iter__ 必须返回 Iterator[Union] 类型。如果您可以通过文档来源确认这一点,那将是一个可以接受的答案。我一直无法这样做,因此提出了这个问题。
  • 是的,它基本上就是这样工作的。文档中没有直接说明这一点,但它隐含在使用 iterationiterable unpacking 的定义中。
  • 所以,这里是赋值语句的文档:docs.python.org/3/reference/simple_stmts.html 相关部分在“else”要点中:

标签: python python-typing iterable-unpacking pyright


【解决方案1】:

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

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

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

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

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

@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"))]]]

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

dataclassesattrsastuple 函数返回的结果都不比 tuple[Any, ...] 好,因此如果我们选择使用这些目标,仍然必须单独注释。

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

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

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

作为最后一点,attrs Why not? page 提到“为什么不命名元组?”,

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

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-09-16
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多