Как внутри работает функция iter()?

Объект итератор - это не фиксированная конструкция вшитая в питон, а скорее объект построенный по определенным правилам. Правила эти подразумевают, что в iter() будет передаваться, либо объект итератор, либо объект-коллекция. Если это объект итератор, то есть объект, в классе которого описан метод next, определяющий условия выдачи следующего элемента и поднятия исключения StopIteration, когда эти элементы себя исчерпают, то что делает с ним функция iter()?Просто передаёт дальше?. Так же не ясно, что делает функция iter() с объектом-коллекцией. Приведу пример с циклом for in, который по идее сначала пропускает передаваемый в него объект(после in) через функцию iter(), а затем через функцию next() и останавливает перебор элементов, при вызове StopIteration:

class A:
    def __init__(self, qty):
        self.qty = qty

    def __iter__(self): #Перегружаем оператор взаимодействия с функцией iter(), чтобы он возвращал сам экземпляр т.к. таковой уже является объектом итератором благодаря методу __next__
        return self

    def __next__(self):
            if self.qty > 0 :
                self.qty -= 1
                return '*'
            else : raise StopIteration

a = A(5)
for i in a
    print(i)
#Вывод:
#*
#*
#*
#*
#*

Однако мы можем не использовать метод next:

class A:
    def __init__(self, qty):
        self.qty = qty

    def __iter__(self):
        return iter(range(self.qty)) # Здесь мы как бы возвращаем изначальный функционал функции iter(), применяя его к объекту-коллекции

a = A(5)

for i in a:
    print(i)
#Вывод:
#0
#1
#2
#3
#4

Означает ли это, что любой объект-коллекция по умолчанию несёт в себе метод next, а функция iter() всегда просто возвращает сам объект? Но в таком случае, что означает вывод:

s = [1,2,3,4,5,6]
c = iter(s)
print(type(c))
#Вывод: <class 'list_iterator'>

Что в действительности сделала функция iter() со списком?


Ответы (2 шт):

Автор решения: CrazyElf

<class 'list_iterator'>

Это специальный класс list_iterator, исходник его в варианте интерпретатора CPython можно посмотреть здесь.

С помощью print(dir(c)) можно убедиться, что у него есть методы __iter__ и __next__.

В принципе, тут неоднократно обсуждалось, как корректно реализовывать свой iter() на Python и, если я правильно помню, без использования отдельного класса для этого получалось не очень, именно потому, что нужно сохранять контроль за __next__, а это можно корректно сделать только через новый класс, в котором и будет этот __next__.

→ Ссылка
Автор решения: Stanislav Volodarskiy

Коротко, точно и бесполезно

Если в объекте obj определён __iter__, iter его вызывает.
Если в объекте obj определён __getitem__, iter создаёт обёртку, которая вызывает obj[i] с растущим индексом i, пока тот не выбросит IndexError.
Иначе iter выбрасывает TypeError

Коротко и со смыслом

Если obj – коллекция, obj.__iter__() возвращает итератор, каждый раз новый.
Если obj – итератор, obj.__iter__() возвращает его самого.
Вариант с obj.__getitem__ нужен для обратной совместимости со старым кодом, все нормальные коллекции и итераторы определяют __iter__.

Длинно, с объяснениями

iterable – то, что вы назвали коллекция. Её можно перебирать неограниченное количество раз. Более того, iterable обязана поддерживать одновременный независимый перебор. В примере ниже черепаха и заяц одновременно бегут по одной коллекции с разной скоростью:

a = range(100)  # iterable
hoar = iter(a)
turtle = iter(a)

next(hoar)
print(next(hoar), next(turtle))  # 1 0
next(hoar)
print(next(hoar), next(turtle))  # 3 1
next(hoar)
print(next(hoar), next(turtle))  # 5 2

iterator – поток значений. Это один поток, итератор нельзя размножить, нельзя использовать несколько раз ни одновременно, ни последовательно. В примере ниже заяц и черепаха связаны: вызываете next у зайца, черепаха тоже двигается. Зайцу черепаху не перегнать:

a = iter(range(100))  # iterator
hoar = iter(a)
turtle = iter(a)

next(hoar)
print(next(hoar), next(turtle))  # 1 2
next(hoar)
print(next(hoar), next(turtle))  # 4 5
next(hoar)
print(next(hoar), next(turtle))  # 7 8

Функция iter по iterable строит iterator. Каждый вызов iter создаёт новый итератор. Все эти итераторы обязаны быть независимыми друг от друга.

В iter разрешено передавать iterator. В этом случае она должна вернуть сам этот объект. Она может возвращать и что-нибудь другое, но все эти "другие" должны делить общее состояние.

Зачем итерировать коллекцию много раз? Например, чтобы работал код для обработки всех пар в коллекции:

# a - коллекция
for u in a:
    for v in a:
        print(u, v)

Зачем итератор одноразовый? Чтобы представить данные, которые в принципе нельзя итерировать несколько раз: поток символов из сокета или входного потока. Вы не заставите пользователя вводить всё с начала только потому что вы вызвали iter(sys.stdin).

Чтобы коллекции итерировались, for вызывает iter создавая итератор. Без этого удобства пришлось бы писать for v in iter(a):.

Но и сами итераторы тоже хочется передавать в for. Для этого iter (которая вызывается внутри for) умеет принимать и итераторы тоже и обязана ничего с ними не делать.

Такой дизайн языка, удобно, все к нему привыкли. А вывод отсюда такой: один объект не может быть и коллекцией и итератором. Потому что
вызов iter от коллекции должен создавать независимые итераторы,
вызов iter от итератора должен создавать зависимые итераторы.

→ Ссылка