Есть ли у mypy тип возвращаемого значения, приемлемый для подкласса?

#python #type-hinting #mypy

#python #подсказка типа #mypy

Вопрос:

Мне интересно, как (или если это в настоящее время возможно) выразить, что функция вернет подкласс определенного класса, который приемлем для mypy?

Вот простой пример, когда базовый класс Foo наследуется с помощью Bar и Baz , и есть удобная функция create() , которая вернет подкласс Foo (либо Bar , либо Baz ) в зависимости от указанного аргумента:

 class Foo:
    pass


class Bar(Foo):
    pass


class Baz(Foo):
    pass


def create(kind: str) -> Foo:
    choices = {'bar': Bar, 'baz': Baz}
    return choices[kind]()


bar: Bar = create('bar')
  

При проверке этого кода с помощью mypy возвращается следующая ошибка:

ошибка: Несовместимые типы в присваивании (выражение имеет тип «Foo», переменная имеет тип «Bar»)

Есть ли способ указать, что это должно быть приемлемо / allowable. Что ожидаемый возврат create() функции не является (или может не быть) экземпляром Foo , а вместо этого является ее подклассом?

Я искал что-то вроде:

 def create(kind: str) -> typing.Subclass[Foo]:
    choices = {'bar': Bar, 'baz': Baz}
    return choices[kind]()
  

но такого не существует. Очевидно, что в этом простом случае я мог бы сделать:

 def create(kind: str) -> typing.Union[Bar, Baz]:
    choices = {'bar': Bar, 'baz': Baz}
    return choices[kind]()
  

но я ищу что-то, что обобщает на N возможных подклассов, где N — число, большее, чем я хочу определить как typing.Union[...] тип.

У кого-нибудь есть идеи о том, как сделать это не сложным способом?


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

  1. Обобщить возвращаемый тип:
 def create(kind: str) -> typing.Any:
    ...
  

Это решает проблему с типизацией при присваивании, но является обломком, поскольку уменьшает информацию о типе возвращаемой сигнатуры функции.

  1. Игнорировать ошибку:
 bar: Bar = create('bar')  # type: ignore
  

Это подавляет ошибку mypy, но это тоже не идеально. Мне нравится, что это делает более явным, что bar: Bar = ... это было преднамеренно, а не просто ошибка кодирования, но подавление ошибки все еще не идеально.

  1. Приведите тип:
 bar: Bar = typing.cast(Bar, create('bar'))
  

Как и в предыдущем случае, положительной стороной этого является то, что он делает Foo возврат к Bar присваиванию более преднамеренно явным. Вероятно, это лучшая альтернатива, если нет способа сделать то, о чем я спрашивал выше. Я думаю, что частью моего отвращения к его использованию является неуклюжесть (как в использовании, так и в удобочитаемости) как обернутой функции. Возможно, это просто реальность, поскольку приведение типов не является частью языка — например, create('bar') as Bar , или create('bar') astype Bar или что-то в этом роде.

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

1. Нет, я не хочу этого делать foo: Foo = create('bar') потому что в любом реальном сценарии (не в упрощенном, который я создал выше) Я хочу использовать функциональность, доступную в Bar подклассе, который не существует в родительском классе Foo .

Ответ №1:

Mypy не жалуется на то, как вы определили свою функцию: эта часть на самом деле полностью исправна и безошибочна.

Скорее, он жалуется на то, как вы вызываете свою функцию в присваивании переменной, которое у вас есть в самой последней строке:

 bar: Bar = create('bar')
  

Поскольку create(...) он аннотирован для возврата Foo или любого подкласса foo , присвоение его переменной типа Bar не гарантирует безопасность. Ваши варианты здесь — либо удалить аннотацию (и согласиться с тем, что bar будет иметь тип Foo ), напрямую преобразовать выходные данные вашей функции в Bar , либо полностью переработать ваш код, чтобы избежать этой проблемы.


Если вы хотите, чтобы mypy понимал, что create при передаче строки Bar он будет возвращать именно a "bar" , вы можете как бы взломать это вместе, объединив перегрузки и литеральные типы. Например. вы могли бы сделать что-то вроде этого:

 from typing import overload
from typing_extensions import Literal   # You need to pip-install this package

class Foo: pass
class Bar(Foo): pass
class Baz(Foo): pass

@overload
def create(kind: Literal["bar"]) -> Bar: ...
@overload
def create(kind: Literal["baz"]) -> Baz: ...
def create(kind: str) -> Foo:
    choices = {'bar': Bar, 'baz': Baz}
    return choices[kind]()
  

Но лично я был бы осторожен в чрезмерном использовании этого шаблона — честно говоря, я рассматриваю частое использование этих типов махинаций с типами как нечто вроде запаха кода. Это решение также не поддерживает специальную оболочку произвольного количества подтипов: вам нужно создать вариант перегрузки для каждого из них, который может стать довольно громоздким и подробным.

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

1. Typehint Foo не указывает Foo на его подкласс, который вам нужно использовать typing.TypeVar для такого типа вещей

Ответ №2:

Вы можете найти ответ здесь. По сути, вам нужно сделать:

 class Foo:
    pass


class Bar(Foo):
    pass


class Baz(Foo):
    pass

from typing import TypeVar
U = TypeVar('U', bound=Foo)

def create(kind: str) -> U:
    choices = {'bar': Bar, 'baz': Baz}
    return choices[kind]()


bar: Bar = create('bar')
  

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

1. Для меня это приводит к A function returning TypeVar should receive at least one argument containing the same TypeVar .

Ответ №3:

Я решил аналогичную проблему с помощью typing.Type . В вашем случае я бы использовал его как so:

 class Foo:
    pass

class Bar(Foo):
    pass

class Baz(Foo):
    pass

def create(kind: str) -> typing.Type[Foo]:
    choices = {'bar': Bar, 'baz': Baz}
    return choices[kind]()
  

Кажется, это работает, потому что Type является «ковариантным», и хотя я не эксперт, приведенная выше ссылка указывает на PEP484 для получения более подробной информации

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

1. Это работает только тогда, когда вам нужно возвращать собственно классы, а не их экземпляры