Как написать сигнатуру функции iter с подсказками типов в Python
Что проверяют
Знаете ли вы, что у встроенного iter две формы, и умеете ли выразить это типами: дженериками, @overload, позиционными параметрами. Следом часто спрашивают, чем Iterable отличается от Iterator.
Как отвечать
Сначала поведение. iter(obj) вызывает __iter__, а если его нет, использует старый протокол последовательности: __getitem__ с индексами от нуля до IndexError. iter(func, sentinel) вызывает функцию без аргументов, пока та не вернёт значение, равное sentinel. Пример: iter(partial(f.read, 4096), b"") читает файл блоками.
Две формы с разными типами описываются перегрузками. Над каждой ставится @overload из typing, ниже идёт одна реальная реализация без декоратора. В Python 3.12 и новее с синтаксисом параметров типа:
- первая форма:
def my_iter[T](obj: Iterable[T], /) -> Iterator[T]: ...; - вторая форма:
def my_iter[T](obj: Callable[[], T], sentinel: object, /) -> Iterator[T]: ...; /в конце: параметры только позиционные, как у встроенной функции;- старые версии:
T = TypeVar("T")вместо[T]; - импорт:
Iterable,Iterator,Callableберите изcollections.abc, алиасы изtypingустарели.
Можно показать глубину: typeshed описывает первую форму ещё и отдельным протоколом для объектов только с __getitem__, а для sentinel=None делает перегрузку, где из Callable[[], T | None] получается Iterator[T] без None.
Разница итерируемого и итератора: у Iterator есть __next__, а __iter__ возвращает его самого, поэтому пройти по нему можно один раз. Список итерируемый, но не итератор.
Где спотыкаются
Пишут def iter(obj: Any) -> Iterator. Тип элемента теряется, mypy и pyright ничего не ловят, а заодно затирается встроенное имя.
Смешивают в одной функции return obj.__iter__() и yield. Любой yield делает функцию генератором, и return уже не возвращает итератор, а просто завершает генерацию.
Считают, что isinstance(x, Iterable) отвечает на вопрос «можно ли по этому итерироваться». Проверка смотрит только на __iter__ и не признаёт объект с одним __getitem__, хотя iter() с ним работает. Надёжный способ: вызвать iter(x) и поймать TypeError.