Рекурсивные типы в Python и трудности с определением типа `type(x)(...)`

Пытаясь создать рекурсивные типы для аннотирования вложенной структуры данных, я наткнулся на следующее.

Этот код верен согласно mypy:

IntType = int | list["IntType"] | tuple["IntType", ...]
StrType = str | list["StrType"] | tuple["StrType", ...]


def int2str(x: IntType) -> StrType:
    if isinstance(x, list):
        return list(int2str(v) for v in x)
    if isinstance(x, tuple):
        return tuple(int2str(v) for v in x)
    return str(x)

Но не этот, который должен быть эквивалентен:

IntType = int | list["IntType"] | tuple["IntType", ...]
StrType = str | list["StrType"] | tuple["StrType", ...]

def bad_int2str(x: IntType) -> StrType:
    if isinstance(x, (list, tuple)):
        return type(x)(bad_int2str(v) for v in x)  # error here
    return str(x)

Сообщение об ошибке

line 6: error: Incompatible return value type (
    got "list[int | list[IntType] | tuple[IntType, ...]] | tuple[int | list[IntType] | tuple[IntType, ...], ...]", 
    expected "str | list[StrType] | tuple[StrType, ...]"
)  [return-value]
line 6: error: Generator has incompatible item type 
    "str | list[StrType] | tuple[StrType, ...]"; 
    expected "int | list[IntType] | tuple[IntType, ...]"  [misc]

Я предполагаю, что mypy может сделать вывод, что type(x) — это либо list, либо tuple. Это ограничение mypy или в этом коде есть что-то подозрительное? Если да, то откуда ограничение?

Почему в 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 может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
2
0
88
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Mypy — это средство проверки статического типа, что означает, что он не оценивает type(x)(bad_int2str(v) for v in x) при запуске. Таким образом, Mypy не может знать, что тип выражения — это StrType, при текущих ограничениях реализации. (Mypy считает, что это выражение list[IntType] | tuple[IntType, ...]. См. Ограничения.)

Вы можете дать подсказку на type(x), например, в следующих примерах.

def good_int2str(x: IntType) -> StrType:
    if isinstance(x, (list, tuple)):
        ty: type[list | tuple] = type(x)
        return ty(good_int2str(v) for v in x)
    return str(x)

def another_good_int2str(x: IntType) -> StrType:
    if isinstance(x, (list, tuple)):
        return cast(type[list | tuple], type(x))(
            another_good_int2str(v) for v in x)
    return str(x)

«[...] Mypy не знает, что тип выражения — это StrType»: Неверно; он знает (reveal_type(bad_int2str(v) for v in x) показывает Generator[<StrType>, None, None]), поэтому в первую очередь выдает ошибку «генератор имеет несовместимый тип элемента».

InSync 05.07.2024 13:55

Нет, мое описание не неверно. Mypy определяет правильный тип аргумента генератора, но не для всего выражения вызова конструктора.

relent95 05.07.2024 15:18

Он тоже знает, что (list[<IntType>] | tuple[<IntType>, ...], хотя и не так, как ожидал ОП, С.Терляков объяснил почему), отсюда и ошибка «несовместимый тип возвращаемого значения».

InSync 05.07.2024 15:24

Нет, это не так. Проверьте, что написано в ссылке: «Анализ Mypy ограничивается отдельными символами и не отслеживает взаимосвязи между символами».

relent95 05.07.2024 15:27

Я не думаю, что это имеет какое-либо отношение к рассматриваемому вопросу. Вам нужно будет уточнить: какие «отдельные символы» и какие «отношения» не отслеживает Mypy?

InSync 05.07.2024 15:28

Я имею в виду, что Mypy не может вывести конечный результат c(a) равно StrType, когда тип a — Generator[StrType, None, None], а тип ctype[list[<IntType>]] | type[tuple[<IntType>, ...]]. В этом примере используются два символа: a и c.

relent95 05.07.2024 15:38

... что возвращает нас к ответу С.Т. Терлякова, и нет, раздел «Ограничения» не посвящен отношениям между вызывающими и вызываемыми объектами, такими как ваши a и c. То, что Mypy не может вывести результат с помощью OP, не означает, что он «не может» узнать. Если бы этого не было, тип был бы Any.

InSync 05.07.2024 15:42
Ответ принят как подходящий

Для type(x) не существует стирания типа.

Что должен сказать mypy о следующем?

x: list[int] = [1]
reveal_type(type(x))

Если мы спросим, там будет сказано:

Выявленный тип: «type[builtins.list[builtins.int]]».

Итак, когда вы запрашиваете type(x)(some_strtype_iterator), он справедливо жалуется, что вы пытаетесь создать list[StrType] | tuple[StrType, ...] из итерации IntType.

Обе ошибки намекают на этот факт: mypy думает, что вы возвращаете список или кортеж IntType, поскольку ошибка внутри выражения не приводит к потере уже известного типа. И это также указывает на то, что вы не можете построить список/кортеж IntType из генератора, дающего StrType - вы не собирались строить последовательность IntType, но type(x) требует этого.

Я только что начал обсуждение на типографском форуме, чтобы выяснить причины, по которым не удаляются дженерики для type(x).

Интересный. Оглядываясь назад, это имеет смысл. Есть ли еще где-нибудь задокументированные подобные «конфликты» между проверкой статического типа и поведением во время выполнения?

Winks 05.07.2024 05:28

@Winks В этом случае поведение зависит от реализации. Pytype, например, передает ваш bad_int2str(). Подобные случаи можно найти в различных обсуждениях, например, в вопросах GitHub, но полной документации пока нет (даже спецификаций).

InSync 05.07.2024 14:09

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