Вот простая функция, которая получает либо экземпляр Foo, и в этом случае она возвращает экземпляр, либо экземпляр класса, который может построить Foo, и в этом случае она строит и возвращает новый экземпляр Foo.
Логика проста, и код работает без ошибок, но и pylance, и mypy жалуются, что мои возвращаемые типы не соответствуют подсказке типа в подписи.
# python 3.9.5
from typing import TypeVar, Union
class Foo:
pass
class FooBuilder:
def build(self) -> Foo:
return Foo()
_F = TypeVar("_F", bound=Foo)
_FB = TypeVar("_FB", bound=FooBuilder)
def get_foo(x: Union[_F, _FB]) -> _F:
if isinstance(x, Foo):
return x # <<< mypy error #1
elif isinstance(x, FooBuilder):
return x.build() # <<< mypy error #2; pylance error
assert isinstance(get_foo(Foo()), Foo) # passes without error
assert isinstance(get_foo(FooBuilder()), Foo) # passes without error
Пиланс жалуется только на второе возвращение:
Expression of type "Foo*" cannot be assigned to return type "_F@get_foo"
В то время как mypy жалуется на оба:
main.py:19: error: Incompatible return value type (got "Foo", expected "_F") [return-value]
main.py:21: error: Incompatible return value type (got "Foo", expected "_F") [return-value]
Ни одна из ошибок не была полезной при поиске, за исключением того, что она определила, что возвращаемые типы не соответствуют сигнатуре, но мне кажется, что они соответствуют.
В реальной жизни я передаю экземпляры, которые наследуются от Foo и FooBuilder, таким образом, используя TypeVar, вместо того, чтобы просто использовать классы непосредственно в сигнатуре функции. Кажется неправильным, что один из моих типов (_FB) присутствует в подписи только один раз, а не дважды, поэтому я обычно использую TypeVar; однако важно, чтобы функция принимала два разных типа, каждый с разными базовыми классами, но возвращала только экземпляр, не являющийся сборщиком. Есть ли что-то кроме TypeVar, которое я должен использовать для этого?






Проблема заключается не только в аннотации функции get_foo, но и в защите вашего типа внутри ее тела.
Остановимся пока только на первом случае. Вы указали, что можете передать экземпляр _F в get_foo, и он вернет экземпляр того же типа _F. Именно для этого предназначена переменная типа в сигнатуре функции: привязка к типу(ам) переданного(ых) аргумента(ов) и соответствующее свертывание возвращаемого типа.
Допустим, у вас есть класс FooSub, наследуемый от Foo, и вы передали экземпляр FooSub в get_foo. Переменная типа _F свернется в FooSub, и ожидается, что возвращаемый тип будет FooSub.
Но ваша проверка isinstance(x, Foo) в теле функции действует как защита типа. После этой проверки средство проверки типов Mypy предполагает, что x относится к типу Foo, а не к тому, чем был _F. Как мы установили, вы могли передать объект FooSub, что потребовало бы от вас возврата объекта FooSub. Поскольку FooSub является подтипом и, следовательно, более узким, чем Foo, возвращение экземпляра Foo небезопасно для типов.
<EDIT> Конкретный способ, которым средство проверки типов должно интерпретировать эту защиту типа, является предметом обсуждения/реализации. Возможно, самый разумный подход — по-прежнему считать x типом _F, потому что верхняя граница, определенная для _F, равна Foo. Pyright, кажется, уже рассматривает это как таковое. Но для Mypy это пока нерешенная проблема. Спасибо @SUTerliakov за указание на это в комментарии.
Другими словами, ваше использование чека isinstance само по себе не является неправильным. Это просто недостаточно ясно для некоторых средств проверки типов при наличии переменной типа. </РЕДАКТИРОВАТЬ>
Если вы знаете, что делаете, и этот isinstance подход действительно необходим, вы можете просто cast преобразовать переменную x в тип _F перед возвратом или использовать директиву type: ignore. Но я бы не рекомендовал этого, потому что сомневаюсь, что проверка isinstance вообще необходима. Это подводит меня ко второму случаю.
FooBuilder, как вы показали выше, не является общим. Его возвращаемый тип build фиксируется на Foo. Следовательно, какой бы подтип FooBuilder не передавался в get_foo, результат вызова его метода build всегда будет выводиться как экземпляр Foo и ничего другого, особенно _F.
Итак, предполагая, что подклассы FooBuilder на самом деле могут возвращать разные типы (подклассы Foo), вам, вероятно, все равно следует сделать FooBuilderуниверсальным. Хотя мое предложенное ниже решение работает нормально, даже если FooBuilder не является общим.
Затем, если все, что вам нужно во втором случае использования, это то, что объект, переданный в get_foo, имеет build метод и является универсальным с точки зрения типа возвращаемого значения этого метода, вы можете определить универсальный протокол для этого. Эта часть действительно важна.
Наконец, если вы сделаете этот протокол runtime_checkable, вы можете начать с isinstance проверки этого протокола, делать то, что вам нужно, с этим объектом построителя и в конечном итоге возвращать его build выходные данные. Поскольку это покрывает один из двух возможных типов объединения, экземпляром которого является аргумент x, средство проверки типов будет знать, что оставшаяся ветвь (эквивалентная предыдущей проверке isinstance, возвращающей False) покрывает другой тип, а именно _F. Поэтому вам не нужно явно проверять это снова. Вместо этого вы можете просто предположить, что x будет типа _F.
В целом, я бы рекомендовал что-то вроде этого:
from typing import Generic, Protocol, TypeVar, Union, runtime_checkable
class Foo:
...
_F = TypeVar("_F", bound=Foo, covariant=True)
class FooBuilder(Generic[_F]):
"""Example of the actual builder base class"""
def __init__(self, foo_cls: type[_F]) -> None:
self.foo_cls = foo_cls
def build(self) -> _F:
return self.foo_cls()
@runtime_checkable
class BuildsFoo(Protocol[_F]):
def build(self) -> _F: ...
def get_foo(x: Union[_F, BuildsFoo[_F]]) -> _F:
if isinstance(x, BuildsFoo):
return x.build()
return x
(Это буквально многоточие ... в методе BuildsFoo.build.)
Демонстрация использования:
...
class Bar(Foo):
...
obj1 = get_foo(Bar())
obj2 = get_foo(FooBuilder(Bar))
reveal_type(obj1) # note: Revealed type is "Bar"
reveal_type(obj2) # note: Revealed type is "Bar"
Вот Mypy Playground с этим кодом.
(Проголосовал за, мой предыдущий комментарий не является полностью саркастическим - вы действительно объясняете, почему сейчас что-то не работает с mypy, пожалуйста, просто упомяните, что это не предназначено)
@SUTerliakov Спасибо, что указали на это. Я включил эти идеи в ответ. Я не полностью согласен с тем, что это однозначно ошибка в том смысле, что это просто теоретически неправильно. Я думаю, что есть место для споров о том, как именно такая защита типа должна влиять на вывод x. Другими словами, это зависит от того, что мы хотим, чтобы думала программа проверки типов. Но я согласен с тем, что подход, уже принятый Pyright, по-прежнему рассматривающий его как _F, вероятно, имеет наибольший смысл на практике. И я надеюсь, что Mypy вскоре последует за ним.
Хороший ответ! Мне нравится, как вы объясняете причину подтвержденной
mypyошибки, которая известна уже несколько лет :) github.com/python/mypy/issues/10817 OPmypyошибки № 1 (в первой условной ветке) не должно быть, это ошибка, и сужение волшебным образом не трансформируетсяxв другой объект. Тот факт, что сужение работает таким образом вmypy(независимо от того, приведение оно вверх или вниз, ваш конечный тип всегда точно соответствует типу 2-го аргументаisinstance), не означает, что вы заменяете переменную типа на ее верхнюю границу. Вместо этого можно было бы запретить присваивание чего-либо не типа_Fx