区分Python函数重载中的同构和异构元组

问题描述 投票:0回答:1

假设我有一个接口

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 错误。

在我的例子中,我怎样才能通过类型注释区分同质和异质元组以捕获正确的输入-返回类型关系?

需要明确的是:实际的运行时代码按照我的意图执行,我只是无法获得正确的类型注释。

python mypy type-hinting
1个回答
0
投票

静态地从

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
    也只是类型精度的损失,而不是完全错误的类型。
© www.soinside.com 2019 - 2024. All rights reserved.