Почему mypy выводит общий базовый тип вместо объединения всех содержащихся типов?

При итерации по разнородной последовательности (например, содержащей элементы типа T1 и T2) mypy предполагает, что целевая переменная имеет тип object (или другой базовый тип, общий для T1 и T2, например, float, если элементы были 1 и 1.2):

xs = [1, "1"]
for x in xs:
    reveal_type(x)  # note: Revealed type is 'builtins.object*'

Разве не было бы более разумным, чтобы предполагаемый тип был Union[T1, T2]? Затем, если и T1, и T2 имеют какой-то общий атрибут, которого не хватает общему базовому классу, телу цикла будет разрешен доступ к этому атрибуту без раздражающих приведений или утверждений isinstance.

Почему mypy выводит здесь один общий базовый тип вместо Union?


person Ash    schedule 11.08.2019    source источник


Ответы (1)


Выбор общего базового класса элементов списка (выбор соединения) вместо объединения элементов - это сознательный выбор дизайна, сделанный mypy.

Короче говоря, проблема в том, что независимо от того, какое из двух решений вы выберете, вы всегда будете сталкиваться с крайними случаями, которые в конечном итоге кому-то будут неудобны. Например, вывести объединение было бы неудобно в следующих случаях, когда вы хотите изменить или добавить в список, а не только читать его:

class Parent: pass
class Child1(Parent): pass
class Child2(Parent): pass
class Child3(Parent): pass

# If foo is inferred to be type List[Union[Child1, Child2]] instead of List[Parent]
foo = [Child1(), Child2()]

# ...then this will fail with a type error, which is annoying.
foo.append(Child3())

Возможно, mypy может попытаться применить какую-нибудь умную эвристику, чтобы определить, следует ли выводить соединение или объединение, но это, вероятно, в конечном итоге будет довольно запутанным и трудно предсказуемым для конечных пользователей.

Это также довольно простая проблема, которую можно обойти на практике - например, вы можете просто добавить явную аннотацию к своей переменной:

from typing import Union, Sized, List

# If you want the union
xs: List[Union[int, str]] = [1, "1"]

# If you want any object with the `__len__` method
ys: List[Sized] = [1, "1"]

Итак, учитывая эти два фактора, реализация какой-нибудь причудливой эвристики или полное переключение на вывод объединений (и нарушение большого количества существующего кода) на самом деле не стоит того.

person Michael0x2a    schedule 12.08.2019
comment
Как обычно, отличный ответ. Я всегда думал, что поведение похоже на дженерики Java, например. нетипизированный List является псевдонимом для List<Object>, меня удивило, что mypy вместо этого выбирает общий класс-предок - спасибо, что указали на это. - person hoefling; 12.08.2019
comment
@hoefling - Вы близки - тип List на самом деле является псевдонимом для List[Any], где Any - динамический тип. (В документации mypy есть дополнительная информация о Any vs object здесь и здесь). Однако на самом деле вы никогда не писали List или какие-либо другие подсказки типа в своем примере - это псевдонимы не имеет значения. Вместо этого средство проверки типов отвечает за выбор того, какой тип xs должен иметь. И mypy обычно склоняется к выводу конкретных, нединамических типов. - person Michael0x2a; 12.08.2019
comment
Стоит отметить, что это специфическое для mypy решение - PEP 484 на самом деле не требует какой-либо конкретной стратегии вывода, поэтому для средства проверки типов было бы так же справедливо решить, что xs является типом List[Any] или List[Union[int, str]]. Например, pyre Facebook приняли противоположное решение: они склоняются к заключению союзов, а не к объединениям. Это общий шаблон для PEP 484 / набора PEP - они подробно описывают, что означает конкретная подсказка типа, но оставляют фактический вывод / использование этих подсказок на усмотрение отдельных средств проверки типов. - person Michael0x2a; 12.08.2019