Следующий скрипт:
from collections.abc import Iterable, Iterator
class A(Iterable):
_list: list[int]
def __init__(self, *args: int) -> None:
self._list = list(args)
def __iter__(self) -> Iterator[int]:
return iter(self._list)
a = A(1, 2, 3)
for i in a:
reveal_type(i)
for s, j in zip("abc", a):
reveal_type(j)
дает следующий вывод mypy:
$ mypy test.py
test.py:17: note: Revealed type is "builtins.int"
test.py:20: note: Revealed type is "Any"
Success: no issues found in 1 source file
Почему тип Any
используется при итерации zip
, а не непосредственно на объекте?
Обратите внимание, что подклассы class A(Iterable[int])
позволяют правильно разрешать типы, но здесь вопрос не в этом;)
Обратите внимание, что вы получите желаемое int
, если вообще опустите Iterable
наследование. Это потому, что Iterable
является протоколом для mypy
, поэтому все, что не наследуется от него, будет проверено на соответствие и выведено на наилучшем возможном уровне (структурное подтипирование). То, что имеет __iter__(self) -> int
, является подтипом Iterable[int]
, поэтому его можно считать таковым. Но заполнение переменных типов в базовых классах нарушит PEP484 и поэтому этого не происходит. Если вы наследуете от Iterable[Any]
, у вас, вероятно, есть причина для этого, и mypy
должен ее уважать.
Я не уверен, но думаю, что A
не является достаточно общим (Iterable
эквивалентно Iterable[Any]
). Если вы добавите переменную типа к определению A
, вы получите ожидаемый раскрытый тип.
T = TypeVar('T')
class A(Iterable[T]):
_list: list[T]
def __init__(self, *args: T) -> None:
self._list = list(args)
def __iter__(self) -> Iterator[T]:
return iter(self._list)
Насколько я могу судить, знания возвращаемого типа A.__iter__
недостаточно для того, чтобы zip
привязал int
к собственной переменной внутреннего типа. Не знаю, можно ли это исправить или нет. (Отмечу, что PyRight также сообщает тип j
как Unknown
, который используется для различения различных вариантов использования Any
.)
Случай PyRight проще, поскольку он не печатает строго zip
: Type of "zip" is "type[zip[Unknown]]"
.
mypy
(через typeshed
) сообщает более полный тип, хотя он усложняется тем фактом, что обработка различного количества итераций, которые zip
может принять, является перегрузкой. Соответствующий случай здесь
def [_T_co, _T1, _T2] (typing.Iterable[_T1`-1], typing.Iterable[_T2`-2], *, strict: builtins.bool =)
-> builtins.zip[tuple[_T1`-1, _T2`-2]]`
Когда вы делаете A
полностью универсальным, mypy
может «извлекать» int
из типа a
, чтобы связать int
с _T2`-2
.
zip[Unknown]
в Pyright, по-видимому, это просто ошибка пользовательского интерфейса или что-то в этом роде: Pyright отображает type[...]
для классов, а mypy
всегда расширяет его до сигнатуры конструктора. Попробуйте добавить reveal_type(s)
— он знает, что это str
правильно. В конце концов, он использует (почти) тот же тип, что и AFAIC, и обработка очень похожа на mypy
.
(и mypy
определенно не связывает A
с _T2
, пожалуйста, исправьте это, мы не видим A
как Iterable[A]
, если только A is str
...)
Ах, я не был уверен, что PyRight использует свои собственные спецификации типов для встроенных модулей. (Кроме того, я не был уверен, что PyRight проводит какое-то различие между самим zip
и «насыщенным» экземпляром zip
, но, оглядываясь назад, это могло показаться глупым поступком.)
Никакого специального регистра, просто введенная магия (5 аргументов работают, 6 разрешают любой): pyright-play.net/…
Почему бы A
не быть привязанным к _T2
?
Потому что A
связано с Iterable[_T2]
, и _T2
выводится из этого. Вы же не говорите, что zip
должен создавать (str, A)
кортежи, не так ли?
О верно. Я до сих пор не привык к дженерикам в стиле Python-3.12.
Полностью удалил этот комментарий, так как не уверен, что будет точным повторением той мысли, которую я пытался донести. Может ли mypy
сделать вывод _T2 ~ int
из типа A.__iter__
в ФП, но это просто не так, или есть техническая причина не делать этого?
Есть причина, я прокомментировал ОП и отвечу сегодня. Вы не хотите, чтобы что-то типа Iterable[Any]
дополнительно проверялось на каждом сайте вызова.
(такая же проблема с «новыми» дженериками, честно говоря, они просто выглядят не так...)
Сравнение цикла for
и объекта zip
не является сравнением яблок с яблоками.
Как проверяется for i in X
? По крайней мере, в текущем mypy
источнике цикл for проверяется путем поиска __iter__
подписи типа X и использования его возвращаемого типа. Таким образом, он не смотрит на ваше Iterable
наследование, а находит ближайший __iter__
в MRO, который оказывается тем, который вы предоставили.
Почему это реализовано именно так? Цикл for — это собственная конструкция языка, определенная на синтаксическом уровне — в некотором смысле она более фундаментальна, чем существование некоторых типов протоколов, инкапсулирующих поведение итерации.
Теперь zip
— это просто класс. Он определяет свой собственный метод __iter__
, поэтому for a,b in zip(A, B)
будет проверяться в соответствии с типом возвращаемого значения. Этот класс является универсальным по типу итератора, поддерживая до 5 различных итераций и возвращаясь к общему случаю для 6 и более. Вот как это выглядит: напечатанная постоянная ссылка. Здесь нет никакой черной магии.
Я процитирую соответствующую часть:
class zip(Iterator[_T_co]):
...
@overload
def __new__(cls, iter1: Iterable[_T1], /, *, strict: bool = ...) -> zip[tuple[_T1]]: ...
@overload
def __new__(cls, iter1: Iterable[_T1], iter2: Iterable[_T2], /, *, strict: bool = ...) -> zip[tuple[_T1, _T2]]: ...
....
def __iter__(self) -> Self: ...
def __next__(self) -> _T_co: ...
(есть ветки, зависящие от версии, и другие перегрузки, я их здесь опущу для наглядности)
Вы столкнулись с перегрузкой с двумя аргументами. Он ищет _T1
и _T2
, «решая» следующее уравнение:
Iterable[_T2] = A
Вот в чем виноват: вы заявляете, что A
— это Iterable[Any]
. Пропуск переменных универсального типа полностью эквивалентен использованию вместо них Any
. Здесь нет места для вывода проверки типов. Итак, mypy
радостно говорит: «Ух ты, я уже знаю это A <: Iterable[Any]
, следовательно, _T2 = Any
, следующий». Он не трогает MRO и вообще не изучает семантику Iterable
: он просто хочет знать, какой тип var должен его параметризовать.
На данный момент mypy
уже знает, что A
является подтипом Iterable[Any]
— вы так сказали! Даже если ваш класс вообще не содержит __iter__
, mypy
уже сообщил бы об этом в определении, не нужно здесь повторяться. Было бы расточительно перепроверять соответствие протокола на каждом месте использования, а также возникло бы множество нежелательных ошибок. Представьте, что по какой-то странной причине у вас есть следующий класс:
class B(Iterable[str]):
def __iter__(self) -> Iterator[int]: ... # type: ignore[override]
Упс. Здесь это не имеет смысла, но может произойти в реальной жизни для более сложных протоколов. Если mypy
проверять __iter__
везде, вам придется # type: ignore
проверять все места, где B
используется как Iterable[str]
.
Примечание: Iterable
шарлатан как typing.Protocol
. Если вы вообще не наследуете от него, ваш класс все равно может быть подтипом Iterable
— это называется структурным подтипом. Это даже пройдет assert issubclass(A, Iterable)
! Если вы не объявляете реализацию явно, средства проверки типов должны как можно точнее определить соответствующий тип. Таким образом, ваш случай, написанный без явного наследования Iterable
, приведет к желаемому результату.
Я понимаю вашу проблему с этим. На самом деле я также не уверен, что этот подход полностью оправдан: возможно, цикл for
должен также обрабатывать итерацию как Iterable
, решая то же уравнение? Но именно так ведет себя реализация в настоящее время, и оба подхода, по моему мнению, одинаково действительны.
Сегодня вечером я напишу лучший ответ, но вот резюме:
for i in a
проверяется проверкой типа на уровне типа, просматривая тип возвращаемого члена__iter__
.Iterable
наследование там не имеет значения, никакого сравнения протоколов не происходит. С другой стороны,zip
— это обычная старая функция, объявленная как принимающая аргументыIterable[_T1]
,Iterable[_T2]
и т. д. до 6 (AFAIC) перегрузок. В данном случаеA
интерпретируется какIterable
, а поскольку вы объявилиIterable[Any]
,__iter__
вообще не учитывается.__iter__
проверялся только на соответствие интерфейса - в теле.