Различение однородных и гетерогенных кортежей в перегрузках функций Python

Предположим, у меня есть интерфейс 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, поскольку объединение не фиксирует возможные отношения ввода и типа возвращаемого значения. Я хочу что-то вроде этого

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, кроме Z, и использовать его в качестве входных данных для варианта MixedProduct. Но это тоже не работает, потому что вы можете иметь Z во входных элементах для варианта MixedProduct, но не во всех из них (см. последний пример show_type выше). FWIW при использовании объединения всех реализаций Base (включая Z) для варианта MixedProduct выдает ту же ошибку MyPy.

Как еще я мог бы различать однородные и гетерогенные кортежи с аннотациями типов, чтобы в моем случае определить правильные отношения типов ввода-возврата?

Чтобы внести ясность: фактический код времени выполнения делает то, что я хочу, я просто не могу правильно определить аннотации типов.

Поменяйте местами @overload. Если у вас возникнут какие-либо дальнейшие ошибки mypy, связанные с overload-overlap, # type: ignore ими — в том, что вы пытаетесь сделать здесь, есть фундаментальная небезопасность типа, но это вряд ли существенно повлияет на ваш код, если только у вас не будет много ситуаций, похожих на a: Base = Z() (преобразование экземпляров Z в Base).

dROOOze 02.08.2024 12:05

ах да, спасибо за быстрый ответ! Действительно, у меня было так раньше, и я получил другую ошибку. Как мне устранить эту фундаментальную небезопасность типа? Кстати, если вы напишете ответ, я его приму :-)

Darkdragon84 02.08.2024 12:10
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
2
94
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Статического вывода ZProduct из make_product можно добиться, поменяв местами @overload, например:

@overload
def make_product(item: Z, /, *items: Z) -> ZProduct: ...  # type: ignore[overload-overlap]
@overload
def make_product(item: Base, /, *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 будет простой потерей точности типа, а не совершенно неправильным типом.

большое спасибо! Интересно, я никогда не думал о возможности явно аннотировать Z как Base. Я не думаю, что это когда-нибудь произойдет в моем коде, но полезно об этом знать! Спасибо за ваш совет!

Darkdragon84 02.08.2024 15:28

@ Darkdragon84 Это был урезанный пример того, что может пойти не так. Если у вас был list[Base], который вы накопили с помощью другой операции, и после этой операции список оказался заполнен Z, если вы распакуете список в make_product, результат также будет статически выведен как MixedProduct - что неверно.

dROOOze 02.08.2024 22:36

Я вижу, действительно. Я буду следить за таким случаем. Интересно, что такое, казалось бы, простое определение может иметь такие сложные последствия...

Darkdragon84 06.08.2024 09:32

Другие вопросы по теме