Я использую mypy и столкнулся с неожиданным поведением. Mypy неправильно определяет тип ожидаемого типа
from typing import Generic, TypeVar, Callable, reveal_type
S1 = TypeVar('S1')
F1 = TypeVar('F1')
I = TypeVar('I')
class Node(Generic[I, S1, F1]):
def __init__(self, callback: Callable[[I], S1 | F1]):
self.callback: Callable[[I], S1 | F1] = callback
class Succ1:
...
class Fail1:
...
def func1(_: str) -> Succ1 | Fail1:
return Succ1()
n1 = Node(func1)
res1 = n1.callback("str")
reveal_type(n1)
% mypy isolated_example2.py
isolated_example2.py:25: error: Need type annotation for "n1" [var-annotated]
isolated_example2.py:25: error: Argument 1 to "Node" has incompatible type "Callable[[str], Succ1 | Fail1]"; expected "Callable[[str], Never]" [arg-type]
isolated_example2.py:27: note: Revealed type is "isolated_example2.Node[builtins.str, Any, Any]"
Found 2 errors in 1 file (checked 1 source file)
Я не ожидаю типа Callable[[str], Never] и не вижу причин так думать. В чем может быть проблема?
mypy==1.9.0
Python 3.12.3
Это часть более серьезной проблемы, но я пытаюсь разделить ее на отдельные части, чтобы лучше понять процесс.
UPD: Похоже проблема в Unioning TypeVar. Это минимальный пример воспроизведения ошибки.
from typing import TypeVar
S1 = TypeVar('S1')
F1 = TypeVar('F1')
def func(arg: S1 | F1):
return arg
func(None)
@chepner вау, и это довольно страшно. Невозможно правильно вывести объединение двух переменных несвязанного типа из типа. Я не понимаю, почему Succ1 | Fail1
решает S1 = Succ1, F1 = Fail1
- почему бы и нет, например. S1 = Succ1 | Fail1, F1 = object
? Почему бы не поменять их местами? Вместо этого подайте 3-союз (Succ1 | Fail1 | Fail2
) и увидите, что он выдает те же ошибки. Я не согласен с тем, что поведение Пайрайта здесь может быть правильным или даже желательным (блин, мне слишком часто приходится это говорить в последние недели).
Необходимо прочитать документацию mypy о «Выводе типов и аннотациях типов». Я думаю что:
from typing import Generic, TypeVar, Callable, Union
S1 = TypeVar('S1')
F1 = TypeVar('F1')
I = TypeVar('I')
class Node(Generic[I, S1, F1]):
def __init__(self, callback: Callable[[I], Union[S1, F1]]):
self.callback: Callable[[I], Union[S1, F1]] = callback
В этой части этой аннотацией вы сообщаете mypy, что обратный вызов метода может возвращать объединение типов между S1 и F1.
S1 | F1
— это просто альтернативный синтаксис для Union[S1, F1]
; это изменение не имеет никакого значения.
Union
явно коммутативен, не так ли? Вы не можете просто случайным образом разделить объединение на две упорядоченные части — что, если в классе есть def foo(self) -> S1
?
class Node(Generic[I, S1, F1]):
def __init__(self, callback: Callable[[I], S1 | F1]):
self.callback: Callable[[I], S1 | F1] = callback
def foo(self) -> S1:
# Huh? Succ1? Fail1? Succ1 | Fail1? object? Something else?
raise NotImplementedError
Следует ли S1
решить Succ1
или Fail1
здесь? Почему?
Итак, вам нужен какой-то способ сообщить средству проверки типов, как правильно разбивать переменные. Для этого вы можете использовать переменные связанного типа:
from typing import Generic, TypeVar, Callable, Literal, Protocol, reveal_type
class SuccBase(Protocol):
success: Literal[True]
class FailBase(Protocol):
success: Literal[False]
S1 = TypeVar('S1', bound=SuccBase)
F1 = TypeVar('F1', bound=FailBase)
I = TypeVar('I')
class Node(Generic[I, S1, F1]):
def __init__(self, callback: Callable[[I], S1 | F1]):
self.callback: Callable[[I], S1 | F1] = callback
# Pyright correctly accepts without protocol inheritance
# For mypy need to inherit protocol explicitly (will report a bug tomorrow?)
class Succ1(SuccBase):
success: Literal[True] = True
class Fail1(FailBase):
success: Literal[False] = False
class Fail2(FailBase):
success: Literal[False] = False
def func1(_: str) -> Succ1 | Fail1 | Fail2:
return Succ1()
n1 = Node(func1)
res1 = n1.callback("str")
reveal_type(n1)
игровая площадка mypy , игровая площадка Pyright
Я использую решение типа «дискриминированного объединения» с протоколом выше, но вы также можете потребовать явное наследование (ABC или просто простой FailBase
класс без атрибутов) или использовать разные атрибуты, которые имеют смысл в вашем контексте, или настроить что-то еще. — ключевой момент — нужно четко указать, что должно войти в S1
, а что — в F1
.
Если вы полностью контролируете дизайн API, рассмотрите возможность не делать этого и вернуть правильный тип, подобный Result
(например, https://github.com/rustedpy/result, или внедрить свой собственный — это тривиально). Это позволит, например, сделать никогда невыполнимые функции (-> Result[Succ1, Never]
) более явными. Или просто полностью избавьтесь от Fail1
, не возвращайте исключения, а поднимайте их — Python построен на исключениях, и снижение производительности довольно низкое, если вы не находитесь в жестком цикле.
Примечание: вы видите там Never
только потому, что вывод невозможен. Это побочный эффект; это исчезнет, если вы явно аннотируете n1
.
Спасибо за ответ. Теперь я лучше понимаю процесс
pyright
принимает оба примера.