Возврат дженериков из функций без параметров

#python #python-3.x #type-hinting #mypy #typing

#python #python-3.x #подсказка типа #mypy #ввод

Вопрос:

Я только начинаю разбираться в более сложных типах в Python, в частности, typing.Generic .

Допустим, у меня есть базовый класс и подкласс этого:

 class Base:
    def base_method(self):
        pass


class Sub(Base):
    def sub_method(self):
        pass
 

Теперь я хочу создать класс, который будет иметь переменную экземпляра, которая может быть «a Base или любым его подклассом». Для этого я должен использовать typing.TypeVar и typing.Generic :

 class FullyTypedContainer(Generic[BaseOrSubclass]):
    def __init__(self, p: BaseOrSubclass):
        self._p = p

    @property
    def p(self) -> BaseOrSubclass:
        return self._p
 

Это отлично работает; тип p «передается» через BaseOrSubclass , чтобы языковые серверы, такие как PyLance, видели, что FullTypedContainer(Sub()).p метод вызывается sub_method() , но FullyTypedContainer(Base()).p не вызывается.

Итак, каков тип для этой функции?

 def get_random_list_of_containers():
    a = random.randint(0, 1)
    if a == 0:
        return [FullyTypedContainer(Sub()), FullyTypedContainer(Base())]
    else:
        return [FullyTypedContainer(Base()), FullyTypedContainer(Sub())]

 
  • -> typing.List[FullyTypedContainer] : не указывает тип FullyTypedContainer , и поэтому кажется, что get_list_of_containers()[0].p это рассматривается как тип Any .
  • -> typing.List[FullyTypedContainer[Base]] : «принуждает» все иметь тип Base (not Base или один из его подклассов), и поэтому get_list_of_containers()[0].p считается, что у него никогда не было метода sub_method() .
  • -> typing.List[FullyTypedContainer[typing.Union[Base, Sub]] : кажется, это лучший вариант, но мне потребуется вручную вести список каждого подкласса Base .

Комментарии:

1. Вам действительно нужны списки? Может быть, это должно быть a tuple ?

2. @juanpa.arrivillaga tuple или list не имеет значения; дело в том, что я хочу знать, как ввести значение, Generics возвращаемое из функции без параметров.

3. Ну, это актуально, потому что кортежи могут содержать и могут быть типизированы неоднородно, но списки должны быть однородными

4. @juanpa.arrivillaga Я вижу это сейчас, но на самом деле это противоречит моему вопросу, поэтому я редактирую.

5. Мне не совсем понятно, чего именно вы ожидаете. Поскольку выходные данные являются полностью случайными, проверка типов всегда должна предполагать наихудший случай Base . Даже если это » Base или подкласс», это все равно означает не больше возможностей, чем Base гарантировано. Для проверки типов нет способа узнать, что get_list_of_containers()[0].p это определенно не просто Base . Итак, можете ли вы уточнить, что проверка типов должна выводить из аннотации на практике?

Ответ №1:

Здесь есть две проблемы, и обе проблемы связаны с различием универсальных типов: оба List и ваш пользовательский универсальный тип FullyTypedContainer являются инвариантными.

Предположим Sub , что это подтип Base . Задан общий тип GenType :

  • Если GenType[Sub] это подтип GenType[Base] , то он ковариантен. Многие контейнеры интуитивно ковариантны.
  • Если GenType[Base] это подтип GenType[Sub] , то он контравариантен. Это звучит нелогично, но Callable тип на самом деле контравариантен по отношению к его типам аргументов. Вызываемый объект, который принимает Base аргумент, может использоваться там, где нам требуется вызываемый объект, который принимает Sub аргумент.
  • Если ни одно из вышеперечисленных не выполняется, то GenType является инвариантным.

Тип Python List инвариантен, и различие Generic типов зависит от его TypeVar — и TypeVar s по умолчанию являются инвариантными. Итак, чтобы правильно ввести аннотацию вашей функции, вам нужно внести два изменения:

  • Используйте тип ковариантной последовательности, например Sequence .
  • Сделайте BaseOrSubclass переменную типа также ковариантной.

В коде это будет выглядеть так:

 BaseOrSubclass = TypeVar("BaseOrSubclass", bound=Base, covariant=True)

class FullyTypedContainer(Generic[BaseOrSubclass]):
    def __init__(self, p: BaseOrSubclass): ...

def get_list_of_containers() -> Sequence[FullyTypedContainer[Base]]:
    return [FullyTypedContainer(Sub()), FullyTypedContainer(Base())]
 

Вот более полный пример в mypy-play: https://mypy-play.net/?mypy=latestamp;python=3.10amp;gist=85af8d25521c9d4b8454719685b37fc8


Примечание по List сравнению Sequence :

Технически List не эквивалентно Sequence :

  • List всегда ссылается на тип для встроенного list .
  • Sequence это любой «подобный списку» тип, который определяет __getitem__ , __len__ , несколько других методов, таких как __reverse__ и т.д. count Это также охватывает tuple , str , bytes типы и тому подобное.

Комментарии:

1. List является ли MutableSequence

2. Я не думаю, что это связано с ковариацией или что добавление ковариации решает проблему. Даже с a Sequence[FullyTypedContainer[Base]] и ковариацией вы все равно теряете статические типы каждого элемента (помимо того, что они являются Base s)

3. @joel Но, как вы сказали, списки / последовательности однородны, и просто нет способа кодировать типы каждого отдельного элемента. Sequence[T] означает, что каждый элемент имеет либо тип T , либо некоторый подтип T , который, я думаю, является тем, что хочет OP. Если OP хочет кодировать «только Base и его подтип Sub , и ни один из других подтипов», то я согласен, что List[FullyTypedContainer[Union[Base, Sub]]] это более точный тип, но тогда вам все равно нужно сделать FullyTypedContainer ковариантным (или использовать List[Union[FullyTypedContainer[Base], FullyTypedContainer[Sub]]] ).

4. @ZecongHu хороший момент List[FullyTypedContainer[Union[Base, Sub]] . Я думаю List[FullyTypedContainer[Base]] , что в любом случае лучше. Возможно, проблема в том, что не совсем ясно, что OP хочет, чтобы функция выполняла. Я основываю свой ответ на том факте, с которым они тестируются get_list_of_containers()[0].p . После индексации ковариация не имеет значения

5. извините, как только вы [0].p , ковариация не имеет значения

Ответ №2:

списки однородны по своему типу: все элементы имеют одинаковый статический тип. Лучшее, что вы можете сделать, это List[FullyTypedContainer[Base]] или List[FullyTypedContainer[Union[Base, Sub]] . Или используйте пользовательскую коллекцию, которая позволяет вам привязывать конкретное содержимое к определенным типам и использовать его вместо списка

Комментарии:

1. Отредактировал мой вопрос, чтобы уйти от этого tuple предложения; в реальном случае, с которым я сталкиваюсь, содержимое списка неизвестно во время сценария.