解包对象属性时是否可以维护类型信息?
Is it possible to maintain type information when unpacking object attributes?
假设我有一个对象,它是 class 的实例,如下所示:
@dataclass
class Foo:
bar: int
baz: str
为了方便起见,我使用 dataclasses
,但在这个问题的上下文中,没有要求 class 必须是 dataclass
。
通常情况下,如果我想解压这样一个对象的属性,我必须实现__iter__
,例如如下:
class Foo:
...
def __iter__(self) -> Iterator[Any]:
return iter(dataclasses.astuple(self))
bar, baz = Foo(1, "qux")
但是,从像 pyright 这样的静态类型检查器的角度来看,我现在已经丢失了 bar
和 baz
的任何类型信息,它只能推断是输入 Any
。我可以通过手动创建 iter
元组参数来稍微改进:
def __iter__(self) -> Iterator[Union[str, int]]:
return iter((self.bar, self.baz))
但我仍然没有 bar
和 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
联系起来了。
显然,none这是无法克服的。如果我想使用类型检查器,我可以不使用 unpack 语法,但如果有一种干净的方法,我真的很想这样做。
如果目前无法使用通用方法,可以接受特定于 dataclasses
或更好的 attrs
的答案。
正如juanpa.arrivillaga所指出的,assignment statements docs表示,如果赋值语句的左侧是一个或多个目标的逗号分隔列表,
The object must be an iterable with the same number of items as there are targets in the target list, and the items are assigned, from left to right, to the corresponding targets.
因此,如果想要解压一个裸对象,就必须实现 __iter__
,它总是具有 Iterator[Union[...]]
或 Iterator[SufficientlyGenericSubsumingType]
的 return 类型它包括多种属性类型。因此,静态类型检查器无法有效地推断解压缩变量的特定类型。
据推测,当 tuple
位于赋值的右侧时,即使语言规范表明它将被视为可迭代对象,静态类型检查器仍然可以有效地推断类型其成分。
因此,正如 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"))]]]
没有任何明确的目标注释,前提是我们愿意编写额外的 class 样板文件。
dataclasses
和 attrs
的 astuple
函数 return 都没有比 tuple[Any, ...]
好,所以目标仍然必须单独注释如果我们选择使用这些。
然而,对于列表理解,这些比
bars = [foo.bar for foo in [Foo(1, "qux")]]
?在大多数情况下可能不会。
作为最后的说明,attrs Why not? page 在提到“为什么不命名元组?”时提到,
Since they are a subclass of tuples, namedtuples have a length and are both iterable and indexable. That’s not what you’d expect from a class and is likely to shadow subtle typo bugs.
Iterability also implies that it’s easy to accidentally unpack a namedtuple which leads to hard-to-find bugs.
我不确定我是否完全同意这两点中的任何一点,但对于其他想要走这条路的人来说,需要考虑一些事情。
假设我有一个对象,它是 class 的实例,如下所示:
@dataclass
class Foo:
bar: int
baz: str
为了方便起见,我使用 dataclasses
,但在这个问题的上下文中,没有要求 class 必须是 dataclass
。
通常情况下,如果我想解压这样一个对象的属性,我必须实现__iter__
,例如如下:
class Foo:
...
def __iter__(self) -> Iterator[Any]:
return iter(dataclasses.astuple(self))
bar, baz = Foo(1, "qux")
但是,从像 pyright 这样的静态类型检查器的角度来看,我现在已经丢失了 bar
和 baz
的任何类型信息,它只能推断是输入 Any
。我可以通过手动创建 iter
元组参数来稍微改进:
def __iter__(self) -> Iterator[Union[str, int]]:
return iter((self.bar, self.baz))
但我仍然没有 bar
和 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
联系起来了。
显然,none这是无法克服的。如果我想使用类型检查器,我可以不使用 unpack 语法,但如果有一种干净的方法,我真的很想这样做。
如果目前无法使用通用方法,可以接受特定于 dataclasses
或更好的 attrs
的答案。
正如juanpa.arrivillaga所指出的,assignment statements docs表示,如果赋值语句的左侧是一个或多个目标的逗号分隔列表,
The object must be an iterable with the same number of items as there are targets in the target list, and the items are assigned, from left to right, to the corresponding targets.
因此,如果想要解压一个裸对象,就必须实现 __iter__
,它总是具有 Iterator[Union[...]]
或 Iterator[SufficientlyGenericSubsumingType]
的 return 类型它包括多种属性类型。因此,静态类型检查器无法有效地推断解压缩变量的特定类型。
据推测,当 tuple
位于赋值的右侧时,即使语言规范表明它将被视为可迭代对象,静态类型检查器仍然可以有效地推断类型其成分。
因此,正如 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"))]]]
没有任何明确的目标注释,前提是我们愿意编写额外的 class 样板文件。
dataclasses
和 attrs
的 astuple
函数 return 都没有比 tuple[Any, ...]
好,所以目标仍然必须单独注释如果我们选择使用这些。
然而,对于列表理解,这些比
bars = [foo.bar for foo in [Foo(1, "qux")]]
?在大多数情况下可能不会。
作为最后的说明,attrs Why not? page 在提到“为什么不命名元组?”时提到,
Since they are a subclass of tuples, namedtuples have a length and are both iterable and indexable. That’s not what you’d expect from a class and is likely to shadow subtle typo bugs.
Iterability also implies that it’s easy to accidentally unpack a namedtuple which leads to hard-to-find bugs.
我不确定我是否完全同意这两点中的任何一点,但对于其他想要走这条路的人来说,需要考虑一些事情。