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 шт):
Ниже будет объяснение, почему возврат списка не работает, но в общем правильно будет из __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__, поэтому такая схема не работает.
Есть два разных понятия: итератор (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)