Приведение типов дженериков

Я использую 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)
pyright принимает оба примера.
chepner 21.04.2024 01:00

@chepner вау, и это довольно страшно. Невозможно правильно вывести объединение двух переменных несвязанного типа из типа. Я не понимаю, почему Succ1 | Fail1 решает S1 = Succ1, F1 = Fail1 - почему бы и нет, например. S1 = Succ1 | Fail1, F1 = object? Почему бы не поменять их местами? Вместо этого подайте 3-союз (Succ1 | Fail1 | Fail2) и увидите, что он выдает те же ошибки. Я не согласен с тем, что поведение Пайрайта здесь может быть правильным или даже желательным (блин, мне слишком часто приходится это говорить в последние недели).

STerliakov 21.04.2024 02:26
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
2
93
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Необходимо прочитать документацию 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]; это изменение не имеет никакого значения.
chepner 20.04.2024 22:34
Ответ принят как подходящий

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.

Спасибо за ответ. Теперь я лучше понимаю процесс

Anka 21.04.2024 13:49

Другие вопросы по теме