Имеет ли mypy возвращаемый тип, приемлемый для подкласса? - PullRequest
0 голосов
/ 31 марта 2019

Мне интересно, как (или если это возможно в настоящее время) выразить, что функция вернет подкласс определенного класса, который приемлем для 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")

Есть ли способ указать, что это должно быть приемлемым / допустимым. Что ожидаемое возвращение функции 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, или что-то в этом роде.

Ответы [ 2 ]

2 голосов
/ 31 марта 2019

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

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

bar: Bar = create('bar')

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


Если вы хотите, чтобы mypy понимал, что create будет возвращать определенно Bar, когда вы передадите строку "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]()

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

0 голосов
/ 02 апреля 2019

Таким образом, после дальнейших исследований (и из других ответов здесь) краткий ответ на мой вопрос, по-видимому, не заключался в том, что в mypy / typing нет никакой функциональности для непосредственной поддержки этого случая.

Однако я нашелчетвертый вариант / обходной путь, который я нашел более удовлетворительным, чем первые три обходных пути, которые я включил в свой вопрос.То есть объединить базовый класс с typing.Any.Это позволяет более гибко определять определение типа в присваивании, сохраняя указание типа базового класса, если оно не переопределено присваиванием.Обходной путь выглядит следующим образом:

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


bar: Bar = create('bar')

В результате mypy принимает назначение без ошибки.Это все еще не идеальный обходной путь, но из всех решений, которые он предоставляет, наиболее близко к желаемому использованию, которое я искал.

...