Итератор в цикле отличается от итератора списка
Пытаясь реализовать метод reduce()
из модуля functools
, я столкнулся с непониманием работы итератора. При использовании метода iter()
, мы получаем итератор списка seq
и присваиваем к переменной seqIter
, после чего методом next()
передаем первый элемент списка в переменную value
.
Однако при выполнении цикла for
, у нас используется итератор x
для итерируемого объекта seqIter
, что в свою очередь является итератором списка. Вопрос, чему будет равен x
на каждом кругу цикла?
На данном куске кода, мы видим реализацию метода reduce
для сложения чисел в списке:
def reduce(func, seq):
seqIter = iter(seq)
value = next(seqIter)
for x in seqIter:
value = func(value, x)
return value
list = [1,2,3]
print("Sum seq is:", reduce(lambda x,y: x+y, list))
Результат:
python .\reduceImplemention.py
Sum seq is: 6
В попытках сократить код, получилось следующее:
def reduce(func, seq):
value = next(iter(seq))
for x in iter(seq):
value = func(value, x)
return value
list = [1,2,3]
print("Sum seq is:", reduce(lambda x,y: x+y, list))
Исходя из правила, что интерпретация строки начинается со скобок(как в математике), должно было выполниться выражение iter(seq)
, после чего остальная строка. Также методом iter()
передаем в цикл for
итератор списка seq
. Однако результат становится равным:
python .\reduceImplemention.py
Sum seq is: 7
Возможно, что дело в цикле for
, поэтому создаем переменную seqIter
и присваиваем iter(seq)
и также меняем в цикле на for x in seqIter
. Результат остался тем же:
python .\reduceImplemention.py
Sum seq is: 7
Следовательно вопрос: Почему метод next
не применился в самом начале блока кода метода reduce
. Или дело в условии цикла?
Ответы (2 шт):
вы дважды используете итератор iter(seq)
без сохранения состояния итератора. Когда вы пишете for x in iter(seq)
, вы фактически создаете новый итератор для последовательности seq
в каждый момент цикла, что приводит к тому, что на первом шаге цикла вы не учитываете элемент, который вы уже забрали с помощью next(seqIter)
в первой версии кода. Таким образом, на первом шаге цикла вы начинаете с первого элемента списка, а затем в цикле пропускаете его и начинаете с второго.
чтобы исправить это, нужно использовать один и тот же итератор в обоих случаях. по типу:
def reduce(func, seq):
seqIter = iter(seq)
value = next(seqIter)
for x in seqIter:
value = func(value, x)
return value
list = [1, 2, 3]
print("Sum seq is:", reduce(lambda x, y: x + y, list)) # Результат: 6
тут seqIter
создается один раз, и его состояние сохраняется в ходе выполнения цикла
В качестве чисто академического интереса задался вопросом, можно ли реализовать аналог reduce без итераторов. Решение на итераторах прекрасно, сомнений нет.
Решил реализовать на генераторах.
Вариант reduce1 компактное, но имеет недостаток: в "*_" хранится кортеж ранее вычисленных значений func. Памятуя о 28+ байт, решение сгодится для маленьких списков. Хотя наверняка существуют ситуации, когда подобный побочный эффект будет полезен.
Вариант reduce2 избавлено от недостатка первого варианта, но для прогона генератора требуется холостой цикл for.
Вариант reduce3 требует подключения модуля collections и создание класса deque. Можно создать свой маленький класс, но это загромождает код. Этот вариант что то среднее между предыдущими. По памяти создается "лишний" экземпляр класса deque, но зато deque автоматически потребляет генератор. В deque храниться только одно значение - последний результат вызова func.
P.S. Если кто-то знает более "приличный" вариант получения последнего элемента генератора, буду благодарен.
from collections import deque
def reduce1(func, seq):
value = None
try:
*_, result = (value := x if value is None else func(value, x) for x in seq)
except Exception as exc:
raise TypeError("reduce() of empty iterable with no initial value") from exc
return result
def reduce2(func, seq):
value = None
g = (value := x if value is None else func(value, x) for x in seq)
for _ in g: pass
if value:
return value
else:
raise TypeError("reduce() of empty iterable with no initial value") from None
def reduce3(func, seq):
value = None
g = (value := x if value is None else func(value, x) for x in seq)
result = deque(g,1)
try:
return result.pop()
except Exception as exc:
raise TypeError("reduce() of empty iterable with no initial value") from exc
Старый вариант имеет ряд недостатков, отмеченных в комментариях:
def reduce(func, seq):
value = 0
for x in iter(seq):
value = func(value, x)
return value