TypeError: iter() returned non-iterator of type 'list'

Копаюсь в дандерах iter и next и решил написать свой класс, который при итерации возвращает свои атрибуты.
Самый первый пример, который мне пришёл в голову - это просто сбор всех атрибутов в отдельном листе и вывод их по одному:

class Test(): 
    def __init__(self, name, age): 
        self.name = name
        self.age = age

    def __iter__(self):
        self.index = -1
        self.list_q = [self.name, self.age]
        return self.list_q
        
    def __next__(self):
        index += 1
        yield self.list_q[self.index]
        
        
t = Test('Имя', 10)

for item in t:
    print(item)
    

Но при данном коде у меня получается ошибка:

Traceback (most recent call last):
  File "main.py", line 19, in <module>
    for item in t:
TypeError: iter() returned non-iterator of type 'list'

В чём проблема? Разве метод iter не должен возвращать итератор для перебора объектов? Разве лист не является таким итератором?


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

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

Ниже будет объяснение, почему возврат списка не работает, но в общем правильно будет из __iter__ возвращать self, ну и __next__ нужно переделать, у вас там сразу несколько ошибок:

class Test(): 
    def __init__(self, name, age): 
        self.name = name
        self.age = age

    def __iter__(self):
        self.index = -1
        self.list_q = [self.name, self.age]
        return self

    def __next__(self):
        self.index += 1
        if self.index < len(self.list_q):
            return self.list_q[self.index]
        raise StopIteration

t = Test('Имя', 10)

for item in t:
    print(item)

Если взять от списка итератор, то работать тоже будет, но управлять отдачей из него элементов вы уже не сможете, __next__ писать в вашем классе бесполезно в этом случае, будет использоваться __next__ от итератора списка, а не ваш:

class Test(): 
    def __init__(self, name, age): 
        self.name = name
        self.age = age

    def __iter__(self):
        self.list_q = [self.name, self.age]
        return iter(self.list_q)

t = Test('Имя', 10)

for item in t:
    print(item)

Как пояснил в комментариях insolor, возврат self как и возврат iter от списка работает, потому что питон проверяет, что у возвращаемого из метода __iter__ объекта существует метод __next__ и поэтому его можно использовать как итератор. А когда вы возвращаете просто список, то у него нет метода __next__, поэтому такая схема не работает.

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

Есть два разных понятия: итератор (iterator), итерируемый объект (iterable). У них есть схожие черты (и по тому, и по другому можно пройтись циклом), но есть большая разница.

С точки зрения реализации:

  • итерируемый объект должен реализовать метод __iter__, из которого должен возвращаться итератор (но не другой итерируемый объект). В идеале - при каждом вызове новый итератор.

  • итератор должен реализовывать и метод __iter__ (который должен возвращать сам объект (self)), и метод __next__, который при каждом вызове должен возвращать следующее значение из итератора (или выбрасывает исключение StopIteration).

Чисто технически, каждый итератор можно считать итерируемым объектом - у каждого итератора реализован __iter__ (который, правда, не создает новый итератор). Но не наоборот - не у каждого итерируемого объекта будет метод __next__. Поэтому и не работает возврат list из __iter__ - он является итерируемым объектом, но не итератором.

С точки зрения поведения:

  • После того как итератор создан, по нему можно пройти циклом (или, например, извлекать значения с помощью функции next) только один раз, при попытке пройти повторно итератор больше не возвращает никаких значений - так называемое исчерпание итератора (iterator exhaustion)

  • По итерируемому объекту (кроме итераторов) можно итерироваться много раз - его метод __iter__ каждый раз возвращает новый итератор.

    Хотя итератор формально (по наличию метода __iter__) можно считать итерируемым объектом, но __iter__ у него не создает новый итератор, а просто возвращает сам объект, по которому уже нельзя пройти повторно, если он исчерпан.

По вашему коду:

  • В принципе, если реализуете итератор, лучше не добавлять в __iter__ никакого кода, кроме return self, а всю инициализацию делать в __init__.
  • __next__ должен именно возвращать значения через return, а не через yield. Если добавляете в функцию yield, то она магически становится генератором, который возвращает итератор (generator iterator, частный случай итератора), а вам нужно ровно одно значение, а не новый итератор.

Таким образом, класс-итератор в идеале должен выглядеть так:

class Test(): 
    def __init__(self, name, age): 
        self.name = name
        self.age = age
        self.index = -1
        self.list_q = [self.name, self.age]

    def __iter__(self):
        return self
        
    def __next__(self):
        self.index += 1
        if self.index >= len(self.list_q):
            raise StopIteration
        return self.list_q[self.index]

Если вы реализуете итерируемый объект, то метод __next__ не нужен, но __iter__ должен вернуть итератор. Из списка можно получить итератор передав его в функцию iter:

class Test(): 
    def __init__(self, name, age): 
        self.name = name
        self.age = age
        self.list_q = [self.name, self.age]

    def __iter__(self):
        return iter(self.list_q)
        
        
t = Test('Имя', 10)

for item in t:
    print(item)

Либо можно использовать yield (как я уже писал выше, в этом случае функция превратится в генератор и будет возвращать итератор):

class Test(): 
    def __init__(self, name, age): 
        self.name = name
        self.age = age
        self.list_q = [self.name, self.age]

    def __iter__(self):
        for item in self.list_q:
            yield item
        
        
t = Test('Имя', 10)

for item in t:
    print(item)

Еще один способ - использовать выражение-генератор (generator expresson), оно также является итератором:

class Test(): 
    def __init__(self, name, age): 
        self.name = name
        self.age = age
        self.list_q = [self.name, self.age]

    def __iter__(self):
        return (item for item in self.list_q)
        
        
t = Test('Имя', 10)

for item in t:
    print(item)
→ Ссылка