Тип итератора — Любой в zip

Следующий скрипт:

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]) позволяют правильно разрешать типы, но здесь вопрос не в этом;)

Сегодня вечером я напишу лучший ответ, но вот резюме: for i in a проверяется проверкой типа на уровне типа, просматривая тип возвращаемого члена __iter__. Iterableнаследование там не имеет значения, никакого сравнения протоколов не происходит. С другой стороны, zip — это обычная старая функция, объявленная как принимающая аргументы Iterable[_T1], Iterable[_T2] и т. д. до 6 (AFAIC) перегрузок. В данном случае A интерпретируется как Iterable, а поскольку вы объявили Iterable[Any], __iter__ вообще не учитывается. __iter__ проверялся только на соответствие интерфейса - в теле.

STerliakov 04.07.2024 17:44

Обратите внимание, что вы получите желаемое int, если вообще опустите Iterable наследование. Это потому, что Iterable является протоколом для mypy, поэтому все, что не наследуется от него, будет проверено на соответствие и выведено на наилучшем возможном уровне (структурное подтипирование). То, что имеет __iter__(self) -> int, является подтипом Iterable[int], поэтому его можно считать таковым. Но заполнение переменных типов в базовых классах нарушит PEP484 и поэтому этого не происходит. Если вы наследуете от Iterable[Any], у вас, вероятно, есть причина для этого, и mypy должен ее уважать.

STerliakov 04.07.2024 17:48
Почему в 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
2
93
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Я не уверен, но думаю, что 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.

STerliakov 04.07.2024 17:55

mypy определенно не связывает A с _T2, пожалуйста, исправьте это, мы не видим A как Iterable[A], если только A is str...)

STerliakov 04.07.2024 17:59

Ах, я не был уверен, что PyRight использует свои собственные спецификации типов для встроенных модулей. (Кроме того, я не был уверен, что PyRight проводит какое-то различие между самим zip и «насыщенным» экземпляром zip, но, оглядываясь назад, это могло показаться глупым поступком.)

chepner 04.07.2024 17:59

Никакого специального регистра, просто введенная магия (5 аргументов работают, 6 разрешают любой): pyright-play.net/…

STerliakov 04.07.2024 18:02

Почему бы A не быть привязанным к _T2?

chepner 04.07.2024 18:02

Потому что A связано с Iterable[_T2], и _T2 выводится из этого. Вы же не говорите, что zip должен создавать (str, A) кортежи, не так ли?

STerliakov 04.07.2024 18:03

О верно. Я до сих пор не привык к дженерикам в стиле Python-3.12.

chepner 04.07.2024 18:05

Полностью удалил этот комментарий, так как не уверен, что будет точным повторением той мысли, которую я пытался донести. Может ли mypy сделать вывод _T2 ~ int из типа A.__iter__ в ФП, но это просто не так, или есть техническая причина не делать этого?

chepner 04.07.2024 18:07

Есть причина, я прокомментировал ОП и отвечу сегодня. Вы не хотите, чтобы что-то типа Iterable[Any] дополнительно проверялось на каждом сайте вызова.

STerliakov 04.07.2024 18:08

(такая же проблема с «новыми» дженериками, честно говоря, они просто выглядят не так...)

STerliakov 04.07.2024 18:09
Ответ принят как подходящий

Сравнение цикла 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, решая то же уравнение? Но именно так ведет себя реализация в настоящее время, и оба подхода, по моему мнению, одинаково действительны.

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