Следующий скрипт:
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__проверялся только на соответствие интерфейса - в теле.