Как ввести функцию аннотации, которая выполняет итерацию по экземплярам класса или кортежам одного и того же класса в Python 3?

Я начинаю экспериментировать с аннотациями типов в Python 3, и у меня возникла проблема с функцией exclude_filter, в частности с аннотацией items в следующем фрагменте кода (я публикую это без аннотации). Короче говоря, я пытаюсь перебрать список и отфильтровать некоторые элементы на основе некоторых критериев. И тип элемента в списке - это либо экземпляр класса, либо кортеж этих экземпляров, и в этом случае я ищу критерии только в первом члене кортежа.

@dataclass
class BaseItem:
    name: str
    something: int

def exclude_filter(items, matches):
    def item_matched(item, matches):
        name = item[0].name if isinstance(item, tuple) else item.name
        for match in matches:
            if match in name:
                return True
        return False    
    items[:] = [i for i in items if not item_matched(i, matches)]

FOOS = [BaseItem("1st foo", 10), BaseItem("2nd foo", 11)]
BARS = [BaseItem("1st bar", 20), BaseItem("2nd bar", 22)]
FOOS_AND_BARS = list(zip(FOOS, BARS))

exclude_filter(FOOS, ["1st"])
exclude_filter(BARS, ["2nd"])
exclude_filter(FOOS_AND_BARS, ["1st"])

print(FOOS)
# [BaseItem(name='2nd foo', something=11)]
print(BARS)
# [BaseItem(name='1st bar', something=20)]
print(FOOS_AND_BARS)
# [(BaseItem(name='2nd foo', something=11), BaseItem(name='2nd bar', something=22))]

Я явно ошибся items: List[BaseItem] с результатом:

Argument 1 to "exclude_filter" has incompatible type "List[Tuple[BaseItem, BaseItem]]"; expected "List[BaseItem]"

Итак, я пробовал item: List[Union[BaseItem, Tuple[BaseItem, BaseItem]]]:

Argument 1 to "exclude_filter" has incompatible type "List[BaseItem]"; expected "List[Union[BaseItem, Tuple[BaseItem, BaseItem]]]"

Потом попробовал T = TypeVar("T", BaseItem, Tuple[BaseItem, BaseItem]) и items: List[T] а item: T, но получил:

"Tuple[BaseItem, BaseItem]" has no attribute "name"

Ну, я пробовал еще более непонятные комбинации, но, похоже, ничего не работает. Как правильно аннотировать этот код?


person Tangente    schedule 23.07.2020    source источник
comment
Мой плохой, пропустил тот. Это может быть связано с Почему mypy отклоняет мой «смешанный союз» объявление типа?   -  person MisterMiyagi    schedule 23.07.2020
comment
Да, похоже, это похоже. Но в моем случае я просто не могу заменить List на Sequence, чтобы избавиться от инвариантной проблемы (соответствует Dict и Mapping), потому что я использую синтаксис items[:] в теле функции. Это просто должен быть Список.   -  person Tangente    schedule 23.07.2020
comment
Так что я нахожу решение, но не объяснение. Если я использую TypeVar и заменяю однострочное условное выражение name = item[0].name if isinstance(item, tuple) else item.name на обычный блок if-else, тогда оно работает. Mypy, вероятно, не может обрабатывать условные выражения с помощью isinstance ().   -  person Tangente    schedule 23.07.2020
comment
Обратите внимание, что переменная типа, как показано, соответствует Union[List[BaseItem], List[Tuple[BaseItem, ...]]], а не List[Union[BaseItem, Tuple[BaseItem, BaseItem]]]. Первый не не соответствует вашему описанию списка, возможно, имеющего как пустые, так и вложенные в кортежи BaseItems. Ваши примеры не проверяют это - желателен ли этот вариант использования?   -  person MisterMiyagi    schedule 23.07.2020
comment
В моем случае все списки должны быть однородными, поэтому решение TypeVar действительно в порядке. Интересно, что List[Tuple[BaseItem, ...]] несовместим с List[Tuple[BaseItem, BaseItem]] из-за этой инвариантной проблемы. Он работает с Sequence, но не с List. Более того, List[Tuple[BaseItem, ...]] представляет собой список со всеми кортежами одинаковой длины, или каждый кортеж может иметь разную переменную длину? Я, наверное, начну обсуждение на mypy github.   -  person Tangente    schedule 23.07.2020


Ответы (1)


Что ж, я нашел рабочее, но уродливое решение этой проблемы, и, к сожалению, оно работает только для однородных списков.

BaseItemOrTuple = TypeVar(
    "BaseItemOrTuple",
    BaseItem,
    Tuple[BaseItem, BaseItem],
    Tuple[BaseItem, BaseItem, BaseItem],
    Tuple[BaseItem, BaseItem, BaseItem, BaseItem],
    # Et cetera for longer tuples
)

def exclude_filter(items: List[BaseItemOrTuple], matches: Sequence[str]) -> None:
    def item_matched(item: BaseItemOrTuple, matches: Sequence[str]) -> bool:
        if isinstance(item, tuple): #  cannot use one-liner here due to possible mypy bug
            name = item[0].name 
        else:
            name = item.name
        for match in matches:
            if match in name:
                return True
        return False    
    items[:] = [i for i in items if not item_matched(i, matches)]

Самая большая проблема с инвариантностью List заключается в следующем:

Argument 1 to "exclude_filter" has incompatible type "List[Tuple[BaseItem, BaseItem]]"; expected "List[Tuple[BaseItem, ...]]"

По этой причине я не могу использовать Tuple[BaseItem, ....] в TypeVar и должен явно указать все возможные длины кортежей. Все будет нормально, если я использую Sequence, но из-за работы items[:] я не могу.

Также, вероятно, есть ошибка в mypy с условным выражением и использованием isinstance(), поэтому мне нужно использовать правильный блок if-else.

person Tangente    schedule 23.07.2020