Модуль itertools. Функция chain()

Курс по стандартной библиотеке: https://stepik.org/a/259466?utm_source=proproprogs

Смотреть материал на YouTube | RuTube

Начиная с этого занятия, мы с вами будем рассматривать возможности весьма популярного модуля itertools стандартной библиотеки Python. Этот модуль включает ряд широко используемых функций для обработки последовательностей с использованием механизма итераторов. Напомню, что итератор – это универсальный интерфейс для перебора элементов любых типов последовательностей. К тому же итераторы не хранят все данные сразу в памяти, а генерируют их по мере необходимости. Благодаря этому эффективно расходуется память устройства.

Объект-итератор в языке Python для любых итерируемых объектов можно получить с помощью функции iter. Например:

d = {'a': 1, 'b': 2}
lst = [1, 2, 3]
s = "hello" 
 
it1 = iter(d)  # итератор для перебора словаря
it2 = iter(lst)  # итератор для перебора списка
it3 = iter(s)  # итератор для перебора строки

А затем, перебирать элементы, вызывая функцию next:

v = next(it3)  # 'h'
v = next(it3)  # 'e'

На основе таких итераторов построены функции модуля itertools. Их можно разделить на следующие группы:

  • объединение и разделение итераторов: chain(), zip_longest(), …
  • комбинирование данных: product(), permutations(), combinations(), …
  • фильтрация и выборка элементов: filterfalse(), dropwhile(), takewhile(), ...
  • инфинитные (бесконечные) генераторы: count(), cycle(), repeat(), ...
  • дополнительные функции: groupby(), starmap(), …

Рассмотрение начнем с группы объединения и разделения итераторов.

Функция chain()

Первая функция chain() позволяет объединять несколько итерируемых объектов в единую последовательность. Пусть, например, имеются следующие объекты:

lst = [1, 2, 3]
s = {'a', 'b', 'c', 'd'}
d = {'key1': 1, 'key2': 2}

Все их можно объединить и перебрать в виде единой последовательности следующим образом:

chained_it = itertools.chain(lst, d, s)

Получаем итератор chained_it, с помощью которого можно перебирать элементы. Сделать это можно несколькими способами. Либо воспользоваться циклом for:

for x in chained_it:
    print(x, end=" ")

Увидим вывод:

1 2 3 key1 key2 b d c a

Либо на основе chained_it сформировать какую-либо новую коллекцию, например, список:

chained_lst = list(chained_it) # [1, 2, 3, 'key1', 'key2', 'a', 'd', 'c', 'b']

Обратите внимание, что объединенные данные возвращаются строго в порядке их записи в функции chain(): список, словарь, множество. И это всегда так. Если порядок изменить, то изменится и порядок следования элементов:

chained_it = itertools.chain(d, s, lst)
chained_lst = list(chained_it) # ['key1', 'key2', 'b', 'c', 'd', 'a', 1, 2, 3]

Таким образом, мы получаем возможность эффективно (с точки зрения удобства и расхода памяти) обрабатывать смешанные структуры данных единым способом. Соответственно, функция chain() применяется в задачах, где требуется условное объединение данных (на уровне итераторов) с их последующей обработкой. Классический пример – это чтение данных из нескольких файлов с последующим объединением результата:

import itertools
 
with open('words1.txt', encoding='utf-8') as f1, open('words2.txt', encoding='utf-8') as f2:
    lines = itertools.chain(f1.readlines(), f2.readlines())
 
for line in lines:
    print(line.strip())

Здесь читаются строки из двух файлов 'words1.txt' и 'words2.txt', а затем, формируется общий итератор lines для последовательного перебора обоих списков циклом for.

Объединение вложенных данных

До сих пор мы использовали одномерные последовательности для их объединения. А что будет, если в функцию chain() передать вложенные списки? Давайте посмотрим. Пусть у нас задан генератор вложенных списков и обычный двумерный список с названиями городов и стран:

data_gen = ([x, x ** 2] for x in range(1, 7))
data_geo = [["Уфа", "Пермь", "Дубай"], ["Россия", "ОАЭ", "Китай"]]

Тогда их объединение:

chained_it = itertools.chain(data_gen, data_geo)
chained_lst = list(chained_it)
print(chained_lst)

даст следующий результат:

[[1, 1], [2, 4], [3, 9], [4, 16], [5, 25], [6, 36], ['Уфа', 'Пермь', 'Дубай'], ['Россия', 'ОАЭ', 'Китай']]

Вполне ожидаемо. Мы видим соответствующие элементы заданных одномерных последовательностей. То есть, вложения, даже если это итерируемые объекты, не перебираются. Однако здесь для их перебора можно воспользоваться функцией chain.from_iterable(), которая дополнительно объединит вложенные итерируемые объекты:

chained_it = itertools.chain.from_iterable(data_gen)

Получим результат:

[1, 1, 2, 4, 3, 9, 4, 16, 5, 25, 6, 36]

Функция chain.from_iterable() принимает только один итератор или итерируемый объект, объединяя вложенные итерируемые объекты. Но, если уровней вложенности будет более двух, например:

data = [list(data_gen), data_geo]
chained_it = itertools.chain.from_iterable(data)

то рекурсивного перебора вглубь вложений выполняться не будет:

[[1, 1], [2, 4], [3, 9], [4, 16], [5, 25], [6, 36], ['Уфа', 'Пермь', 'Дубай'], ['Россия', 'ОАЭ', 'Китай']]

Таким образом, функция chain() помогает гибко и эффективно объединять различные последовательности данных, позволяя избегать затрат ресурсов на создание огромных временных структур данных. Она отлично подходит для ситуаций, связанных с обработкой разнотипных коллекций, особенно при ограничениях памяти и производительности.

Курс по стандартной библиотеке: https://stepik.org/a/259466?utm_source=proproprogs

Видео по теме