[ Сборник задач ]
Тема 15. Итераторы

[ Сборник задач ]
Тема 15. Итераторы

Python Workbook Cover T1
Операторы: iter
Контент: Вопросы (5шт) + задачи (5шт)

Оглавление

1
Введение
Понятие итераторов и итерируемых объектов, их различия, специфика протоколов, особенности применения. Способы создания ленивых итераторов и генераторов.
Перейти
2
Вопросы и ответы
5 вопросов по теме "Итераторы" + ответы
Перейти
3
Условия задач
5 задач по теме двух уровней сложности: Базовый и *Продвинутый
Перейти
4
Решения задач
Приводим код решений указанных выше задач
Перейти
1
One

Введение

Итераторы – специальные объекты для перебора коллекций. Основная их задача – упростить навигацию по отдельным значениям последовательностей или словарей.

Специфика работы итератора такова:
  1. На каждом шаге итерации он обращается к следующему элементу последовательности;
  2. В момент достижения последнего значения возникает ошибка, говорящая о том, что больше элементов нет.
Чаще всего итераторы используются в цикле for, который незаметно для пользователя обрабатывает ошибку окончания коллекции и заканчивает свою работу.

Предлагаемые к решению задания опираются на следующие темы:
  1. Итератор и итерабельный объект, их протоколы;
  2. Функции iter(), next();
  3. Ленивые (lazy) итераторы;
  4. Генераторы (предназначение, yield).

Читайте также

2
Two

Вопросы по теме "Итераторы"

1. Перечислите основные отличия между итератором и итерабельным объектом.
Оба объекта позволяют перебирать коллекции, но их внутренняя логика разнится. Основные отличия приведены ниже.

Итератор
  1. По мере перебора истощается
  2. При повторном обращении продолжает перебор с последнего места остановки
  3. Не индексируется
  4. Для повторного перебора с начала нужно пересоздавать
  5. Занимает мало места в памяти
Итераторы создаются, например, функциями map(), zip().

Итерабельный объект
  1. Никогда не истощается
  2. При повторном обращении в другом месте программы начинает перебирать элементы с начала
  3. Индексируется
  4. Не требует пересоздания для повторной итерации
  5. Занимает больше места в памяти
Итерабельными объектами являются списки, кортежи.
2. В чем заключается смысл протокола итератора?
Любой объект в Питоне становится итератором только тогда, когда в него внедрен соответствующий протокол. Он включает 2 метода:

iter() – возвращает сам себя, вызывается в самом начале. Позволяет пользоваться циклом for и выражением in»;
next() – перебирает коллекцию по одному элементу и вызывает ошибку StopIteration, когда их не остается.

Пример – Интерактивный режим
---
>>> num_lst = [7, 10, 3]
>>> num_lst_iter = iter(num_lst)
>>> next(num_lst_iter)
7
>>> next(num_lst_iter)
10
>>> next(num_lst_iter)
3
>>> next(num_lst_iter)
StopIteration


Обозначенную последовательность чисел мы превратили в итератор с помощью функции iter(). После того как функция next() перебрала все значения, возникла ошибка StopIteration.
3. Приведите несколько примеров встроенных функций-итераторов, используемых в Питоне.
Итераторы используются достаточно часто, так как практически не тратят память компьютера. Именно поэтому в Питон встроено большое количество соответствующих функций. Вот некоторые из них:

open() – для работы с файлами;
zip() – формирует итератор кортежей из представленных последовательностей;
map() – модифицирует каждый элемент итерируемого объекта.

Чтобы проверить, является ли объект итератором, достаточно сравнить его с с самим собой, обернутым в функцию iter().

Пример – Интерактивный режим
---
>>> file = open('article.txt')
>>> iter(file) is file
True
>>> file.close()
>>> lst = [2, 3]
>>> iter(lst) is lst
False

4. Когда применяются ленивые вычисления и в чем их логика?
Ленивые вычисления предполагают, что не нужно ничего делать до тех пор, пока в этом нет необходимости. Так, если мы не обратились к свойству класса (которое определяется некими математическими операциями, например), то не нужно заранее его рассчитывать. На этом базируется, в том числе, логика генераторов. Многие итераторы также являются ленивыми. Это не просто удобно, но позволяет экономить память и время на вычисление.

Нагляднее всего можно понять преимущество ленивых вычислений (и итераторов) на следующем примере.

Пример – Интерактивный режим
---
# Создадим итерабельный объект
>>> nums = range(1, 12121212112)
>>> nums[10543210010]
10543210011 # Практически моментальный вывод
>>> list(nums)
MemoryError


Функция range() – ленивая. Она заранее не формирует последовательность из гигантского количества чисел. А когда мы обращаемся к некому ее элементу по индексу (это разрешено, так как она создает итерабельный объект), он выводится почти сразу.

Если вы рискнете преобразовать этот массив чисел в список, то случится одно из двух: либо выведется ошибка о нехватке памяти на компьютере, либо очень-очень долго будет формироваться список всех объектов.
5*. Дайте пример одного бесконечного итератора, который встроен в Python. Опишите его функционал.
«Ленивость» большинства итераторов и генераторов позволяет создавать «бесконечные» коллекции. Ведь они, по факту, не загружаются полностью в память, а выводятся по одному по мере надобности.

В Python встроено немалое количество функций, позволяющих реализовывать такие сущности. Ряд из них расположен в модуле itertools. Рассмотрим функцию count(). Она формирует бесконечную последовательность чисел с определенным шагом (шаг может быть как целым числом, так и десятичным). Это своего рода «считалка».

Пример – Интерактивный режим
---
>>> from itertools import count
>>> count_begin = count(5, 10)
>>> for num in range(6):
… print(next(count_begin))
5
15
25
35
45
55
>>> for num in range(2):
… print(next(count_begin))
65
75


Повторное обращение к «каунтеру» продолжит счёт с того числа, на котором он остановился до этого.
3
Three

Задачи по теме "Итераторы"

4
Two

Решение

Задача 1. Базовый уровень

Условие
Антонина в целях экономии памяти компьютера вместо последовательности чисел от 1 до миллиона в виде списка решила создать ленивый итератор numbers с помощью функции range(). Затем в двух разных местах своего скрипта она делала проверку того, есть ли число 2000 в этой коллекции. 
В первом случае код вернул True, а во втором False.

Объясните ученице ее ошибку и предложите способ исправления ситуации.
 
Код – IDE
--
numbers = iter(range(1, 1000001))
print(2000 in numbers)
print(2000 in numbers)
 
Результат выполнения
---
True
False
 
В данном случае не было никакого смысла создавать итератор, так как функция range() изначально создает ленивый итерабельный объект, который занимает практически такой же объем памяти.
Решение - IDE
import sys
numbers = iter(range(1, 1000001))
print(sys.getsizeof(numbers))
numbers = range(1, 1000001)
print(sys.getsizeof(numbers))
Результат выполнения
32
48
Как видно, 32 или 48 байт в памяти – никакой разницы, она никак не почувствуется.

Значение False при втором обращении к итератору объясняется тем, что он уже «истощился» – там больше нет числа 2000, как и всех предыдущих, которые меньше.
Решение - IDE
print(next(numbers))
Результат выполнения
2001

Задача 2. Базовый уровень

Условие
Создайте функцию infinite(lst, tries), которая будет проходиться по элементам списка lst (целые числа) заданное количество раз (tries) циклически. 
Один раз - один элемент списка. 
После вывода последнего значения последовательности процедура начнется с самого начала.

Например, если в списке 2 элемента, а функция получила значение 3, то сначала выведется первый объект, потом последний, а потом опять первый. 
Результат работы функции представьте в виде строки, состоящей из tries количества символов.
Для решения задачи нужно использовать функцию cycle() из модуля itertools. Она перебирает последовательность циклически, а по мере достижения последнего элемента начинает заново.
Решение - IDE
from itertools import cycle
 
 
def infinite(lst, iterations):
    result = ''
    iter_lst = cycle(lst)
    if lst:
        for symbol in range(iterations):
            result += str(next(iter_lst))
    return result
 
 
# Тесты
print(infinite([2, 5, 8], 7))
print(infinite([], 1000))
print(infinite([7], 4))
Результат выполнения
2582582
# Пустая строка
7777

Задача 3. Базовый уровень

Условие
Инструкция yield позволяет создавать генераторы. 
В отличие от объявления return в функции, где возвращается один объект, yield при каждом вызове функции генерирует новый объект. 
Фактически это дает возможность использовать генераторы в циклах. 
Самая важная причина применения такой инструкции - экономия памяти, когда не требуется сохранять всю последовательность, а можно получать ее элементы по одному. 

Ученик написал генератор show_letters(some_str), выводящий все символы строки на печать, но только в том случае, если они являются буквами (остальные игнорируются). 
Сократите код функции.
 
Код – IDE
---
def show_letters(some_str):
    clean_str = ''.join([letter for letter in some_str if letter.isalpha()])
    for symbol in clean_str:
        yield symbol
 
Конструкция yield from позволяет полностью убрать цикл из функции. Она "вкладывает" один генератор внутрь другого, что дает возможность управления несколькими генераторами.
Решение - IDE
def show_letters(some_str):
	yield from ''.join([letter for letter in some_str if letter.isalpha()])
 
 
random_str = show_letters('A!sdf 09 _ w')
print(next(random_str))
print(next(random_str))
Результат выполнения
A
s
 

Задача 4. Продвинутый уровень

Условие
Числа Фибоначчи представляют последовательность, получаемую в результате сложения двух предыдущих элементов. 
Начинается коллекция с чисел 1 и 1. 
Она достаточно быстро растет, поэтому вычисление больших значений занимает немало времени. 
Создайте функцию fib(n), генерирующую n чисел Фибоначчи с минимальными затратами ресурсов.
Для реализации этой функции потребуется обратиться к инструкции yield. 
Она не сохраняет в оперативной памяти огромную последовательность, а дает возможность “доставать” промежуточные результаты по одному. 
 
Необходимо превратить функцию в генератор при помощи инструкции yield, чтобы вычисления осуществлялись не сразу, а по мере надобности.
Решение - IDE
def fib(n):
    fib0 = 1
    yield fib0
    fib1 = 1
    yield fib1
    for i in range(n - 2):
        fib0, fib1 = fib1, fib0 + fib1
        yield fib1
 
 
# Тест
for num in fib(112121):
	pass
print(num)
Результат выполнения
3578717901141606491845…

Задача 5. Продвинутый уровень

Условие
Реализуйте итератор колоды карт (52 штуки) CardDeck. Каждая карта представлена в виде строки типа «2 Пик». При вызове функции next() будет представлена следующая карта. По окончании перебора всех элементов возникнет ошибка StopIteration.
Чтобы реализовать протокол итератора требуется внедрить 2 метода: __iter__() и __next__(). Для понимания того, что коллекция иссякла, не стоить забывать и про ее длину (52).
Решение - IDE
class CardDeck:
    def __init__(self):
        self.length = 52
        self.index = 0
        self.__SUITS = ['Пик', 'Бубей', 'Червей', 'Крестей']
        self.__RANKS = [*range(2, 11), 'J', 'Q', 'K', 'A']

    def __len__(self):
        return self.length

    def __next__(self):
        if self.index >= self.length:
            raise StopIteration
        else:
            suit = self.__SUITS[self.index // len(self.__RANKS)]
            rank = self.__RANKS[self.index % len(self.__RANKS)]
            self.index += 1
            return f'{rank} {suit}'

    def __iter__(self):
        return self


deck = CardDeck()
while True:
    print(next(deck))
 
Результат выполнения
2 Пик
3 Пик
4 Пик
…
K Крестей
A Крестей
StopIteration
Как вам материал?

Читайте также