假设我有一个接口
Base
有很多实现
from abc import ABC
class Base(ABC): ...
class A(Base): ...
class B(Base): ...
class C(Base): ...
# ...
class Z(Base): ...
现在我想定义一个复合类来保存此类对象的冻结集。有一个通用接口
Product
和两种实现,它们采用异构冻结集 (MixedProduct
) 或 Z
的同构冻结集 (ZProduct
)
from abc import ABC, abstractmethod
from dataclasses import dataclass
class Product(ABC):
@property
@abstractmethod
def items(self) -> frozenset[Base]: ...
@dataclass(frozen=True)
class MixedProduct(Product):
items: frozenset[Base]
@dataclass(frozen=True)
class ZProduct(Product):
items: frozenset[Z]
有一个工厂函数,它接受任意数量的
Base
对象并返回正确的 Product
对象
from collections.abc import Iterable
from typing_extensions import TypeGuard
def check_all_z(items: tuple[Base, ...]) -> TypeGuard[tuple[Z, ...]]:
return all([isinstance(item, Z) for item in items])
def make_product(*items: Base) -> MixedProduct | ZProduct:
# `items` is a tuple[Base, ...]
if check_all_z(items): # the TypeGuard tells MyPy that items: tuple[Z, ...] in this clause
return ZProduct(frozenset(items))
return MixedProduct(frozenset(items))
因此,仅当所有输入项均为
ZProduct
时,此函数才会返回 Z
,否则返回 MixedProduct
。现在我想缩小 make_product
的返回类型,因为 Union 无法捕获可行的输入 - 返回类型关系。我想要的是这样的
reveal_type(make_product(Z())) # note: Revealed type is "ZProduct"
reveal_type(make_product(A())) # note: Revealed type is "MixedProduct"
reveal_type(make_product(Z(), Z())) # note: Revealed type is "ZProduct"
reveal_type(make_product(B(), A())) # note: Revealed type is "MixedProduct"
reveal_type(make_product(B(), Z())) # note: Revealed type is "MixedProduct" # also contains one Z!!
我继续定义两个重载
from typing import overload
@overload
def make_product(*items: Base) -> MixedProduct: ...
@overload
def make_product(*items: Z) -> ZProduct: ...
def make_product(*items):
if check_all_z(
items
): # the TypeGuard tells MyPy that items: tuple[Z, ...] in this clause
return ZProduct(frozenset(items))
return MixedProduct(frozenset(items))
因此,第一个重载是“包罗万象”,而第二个重载是唯一可以得到
ZProduct
的情况的专门化。但现在 MyPy 抱怨
error: Overloaded function signature 2 will never be matched: signature 1's parameter type(s) are the same or broader [misc]
所以我的问题是,有没有一种方法可以专门针对
make_product
的注释来处理这种特定情况,并以任何其他方式返回 ZProduct
?对于overload
,这似乎只有在所有涉及的类型都没有任何重叠的情况下才有可能。这意味着我必须定义 Base
except Z
的所有其他实现的联合,并将其用作 MixedProduct
变体的输入。但这也不起作用,因为您可以在Z
变体的输入项中具有MixedProduct
,但不是全部(请参见上面最后一个reveal_type示例)。 FWIW 对 Base
变体使用 Z
(包括 MixedProduct
)的所有实现的 Union 会引发相同的 MyPy 错误。
在我的例子中,我怎样才能通过类型注释区分同质和异质元组以捕获正确的输入-返回类型关系?
需要明确的是:实际的运行时代码按照我的意图执行,我只是无法获得正确的类型注释。
静态地从
ZProduct
推断 make_product
可以通过切换 @overload
来实现,如下所示:
@overload
def make_product(*items: Z) -> ZProduct: ... # type: ignore[overload-overlap]
@overload
def make_product(*items: Base) -> MixedProduct: ...
def make_product(*items: Base) -> MixedProduct | ZProduct:
if check_all_z(items):
return ZProduct(frozenset(items))
return MixedProduct(frozenset(items))
# type: ignore[overload-overlap]
正在抑制 mypy 错误,该错误警告您类型不安全:
>>> z1: Base = Z()
>>> z2: Base = Z()
>>> reveal_type(make_product(z1, z2)) # revealed type is "MixedProduct"
这种不安全性非常糟糕,因为
MixedProduct
不是 ZProduct
的超类,因此如果不小心,静态错误很容易在整个代码中传播。
有几种方法可以减少这种不安全性,例如:
Base
(尽可能使用具体类型 A
、B
...),并避免将 A
、B
、...与 Z
并集。这样,当项目传递到 make_product
时,类型检查器将静态地知道您何时传递了纯 Z
对象,并正确地告诉您类型是 ZProduct
。MixedProduct
成为ZProduct
的父类。这样,即使由于 ZProduct
实例被屏蔽为 Z
实例而未拾取 Base
,推断 MixedProduct
也只是类型精度的损失,而不是完全错误的类型。