#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[...]
тип.
У кого-нибудь есть идеи о том, как сделать это не сложным способом?
В вероятном случае, когда не существует простого способа сделать это, мне известно о ряде неидеальных способов обойти проблему:
- Обобщить возвращаемый тип:
def create(kind: str) -> typing.Any:
...
Это решает проблему с типизацией при присваивании, но является обломком, поскольку уменьшает информацию о типе возвращаемой сигнатуры функции.
- Игнорировать ошибку:
bar: Bar = create('bar') # type: ignore
Это подавляет ошибку mypy, но это тоже не идеально. Мне нравится, что это делает более явным, что bar: Bar = ...
это было преднамеренно, а не просто ошибка кодирования, но подавление ошибки все еще не идеально.
- Приведите тип:
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. Это работает только тогда, когда вам нужно возвращать собственно классы, а не их экземпляры