Перевод статьи Tour of Python Itertools.
В этой статье мы рассмотрим возможности модулей itertools и more_itertools, а также покажем как использовать их на практике для обработки последовательностей данных.
Для языка Python разработано множество отличных библиотек, но большинство из них по функциональности даже не приближаются к тем, что встроены непосредственно в стандартную библиотеку, например, модуль itertools. В свою очередь модуль more_itertools, как следует из названия, является его гармоничным дополнением. Эти два модуля предоставляют инструментарий, по аналогии сопоставимый с функциональностью кухонного комбайна, когда дело доходит до обработки/итерации последовательностей данных. Тем не менее, на первый взгляд, не все функции из их состава могут показаться полезными на практике. Поэтому давайте сделаем небольшой тур по наиболее интересным, а также примерам того, как можно использовать их с максимальной эффективностью!
Содержание
itertools
Compress
Модуль itertools предоставляет немало полезных функций для фильтрации последовательностей значений, одна из них compress
. В качестве аргументов она принимает два итератора. В первый аргумент передается фильтруемая последовательность, а во второй последовательность булевых значений, которая по сути является селектором. Результатом работы функции будет последовательность, содержащая элементы первой, соответствующие значениям True
из второй, селектирующей последовательности.
dates = [ "2020-01-01", "2020-02-04", "2020-02-01", "2020-01-24", "2020-01-08", "2020-02-10", "2020-02-15", "2020-02-11", ] counts = [1, 4, 3, 8, 0, 7, 9, 2] from itertools import compress bools = [n > 3 for n in counts] print(list(compress(dates, bools))) # функция compress возвращает итератор! # ['2020-02-04', '2020-01-24', '2020-02-10', '2020-02-15']
На практике можно использовать этот прием для того, чтобы применить результат фильтрации одной последовательности к другой, как в примере выше. Сначала мы создаем список дат, а затем селектируем его в соответствии с результатом обработки последовательности целых значений, из которой выбираются значения большие 3
.
Accumulate
Как следует из названия этой функции мы будем использовать ее для аккумуляции (накопления) результатов работы некоторой функции. Отличным примером ее использования может послужить следующий код, который позволяет находить и перемещать текущее максимальное значение по результирующей последовательности. Или получим последовательность промежуточных значений в ходе вычисления факториала заданного числа:
from itertools import accumulate import operator data = [3, 4, 1, 3, 5, 6, 9, 0, 1] list(accumulate(data, max)) # перемещаем максимум по последовательности # [3, 4, 4, 4, 5, 6, 9, 9, 9] list(accumulate(range(1, 11), operator.mul)) # факториал # [1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800]
Как видим вторым аргументом в accumulate
передается функция, которая выполняется для каждого текущего значения входной последовательности.
Если вас не интересуют промежуточные результаты вычислений, вы можете использовать функцию reduce
(называемый также fold
в других языках программирования). Результат выполнения этой функции сохраняет только конечное значение и поэтому более эффективен с точки зрения использования памяти.
Cycle
В функцию cycle
передается итератор, на основе которого она создает бесконечный цикл. Это может быть полезно, например, в игре, где игроки делают ход по очереди. Еще одна интересная вещь, которую вы можете сделать с помощью cycle
– создать бесконечный спиннер (вращающий символ слеша, отображающийся в терминале):
# цикл по игрокам from itertools import cycle players = ["John", "Ben", "Martin", "Peter"] next_player = cycle(players).__next__ player = next_player() # "John" player = next_player() # "Ben" # ... # бесконечный спиннер import time for c in cycle('/-\|'): print(c, end = '\r') time.sleep(0.2)
Tee
И хотя модуль itertools
содержит много других инструментов, рассмотрим в этой статье последнюю и, на мой взгляд, просто замечательную функцию из его состава. Это функция tee
, которая создает несколько независимых итераторов на основе одного. Итераторы, возвращаемые функцией tee
, могут быть использованы c целью передачи одного и того же набора данных нескольким отдельно работающим алгоритмам для их последующей параллельной обработки.
Примером ее использования на практике может послужить функция pairwise
, которая возвращает пары значений из итерируемого ввода (текущее и предыдущее значение):
from itertools import tee def pairwise(iterable): """ s -> (s0, s1), (s1, s2), (s2, s3), ... """ a, b = tee(iterable, 2) next(b, None) return zip(a, b)
Эта функция удобна если вам нужно получить несколько отдельных указателей на один и тот же поток данных. Однако будьте осторожны при его использовании, поскольку он может быть довольно дорогостоящим, когда дело касается памяти. Также важно отметить, что вы не должны использовать исходный объект итератор после передачи его в функцию tee
, поскольку можете его “испортить”. Например, непреднамеренно сдвинуть указатель на текущее значение в новых итераторах.
more_itertools
Теперь давайте подробнее рассмотрим, что предлагает модуль more_itertools
, так как в ней есть много интересных и полезных функций, о которых вы, возможно, и не слышали.
Divide
И так знакомство с модулем more_itertools
начнем с функции divide
. Как следует из названия, ее использование позволяет разделить процедуру итерации на заданное количество, так сказать, под-итераций (итераторов). Как вы можете видеть из примера ниже, в результате ее выполнения длина полученных итераторов может получиться различной, так как будет зависеть от количества элементов в исходной последовательности и, указанного при вызове функции, количества под-итераций.
from more_itertools import divide data = ["first", "second", "third", "fourth", "fifth", "sixth", "seventh"] [list(l) for l in divide(3, data)] # [['first', 'second', 'third'], ['fourth', 'fifth'], ['sixth', 'seventh']]
Partition
С помощью этой функции мы также можем разделить наш итератор на отдельные части, но на этот раз с использованием предиката (логического условия):
# Разделение последовательности дат по срокам from datetime import datetime, timedelta from more_itertools import partition dates = [ datetime(2015, 1, 15), datetime(2020, 1, 16), datetime(2020, 1, 17), datetime(2019, 2, 1), datetime(2020, 2, 2), datetime(2018, 2, 4) ] # определим функцию генерирующую дату на месяц позже от текущей is_old = lambda x: datetime.now() - xВ первом примере мы разделяем список дат на «новые» и «старые», используя значение текущей даты, генерируемое лямбда-функцией.
Во втором примере разделяем файлы на две группы с “разрешенными” и “запрещенными” расширениями. Для этого снова используется лямбда-функция, которая разделяет строку с именем файла на имя и расширение, а также проверяет находится ли его расширение в списке “разрешенных”.
Consecutive_groups
Если вам необходимо найти в последовательности данных серии следующих подряд чисел, дат, букв, логических значений или любых других упорядочиваемых объектов, то вы можете воспользоваться функцией
consequence_groups
:import datetime import more_itertools import pprint dates = [ datetime.datetime(2020, 1, 15), datetime.datetime(2020, 1, 16), datetime.datetime(2020, 1, 17), datetime.datetime(2020, 2, 1), datetime.datetime(2020, 2, 2), datetime.datetime(2020, 2, 4) ] ordinal_dates = [] for d in dates: ordinal_dates.append(d.toordinal()) groups = [list(map(datetime.datetime.fromordinal, group)) for group in more_itertools.consecutive_groups(ordinal_dates)] pprint.pprint(groups) #[[datetime.datetime(2020, 1, 15, 0, 0), datetime.datetime(2020, 1, 16, 0, 0), datetime.datetime(2020, 1, 17, 0, 0)], # [datetime.datetime(2020, 2, 1, 0, 0), datetime.datetime(2020, 2, 2, 0, 0)], # [datetime.datetime(2020, 2, 4, 0, 0)]]И так имеется список дат, в котором некоторые из них идут подряд. Получим список всех последовательностей (серий) дат, значения которых идут подряд.
И первое что нужно сделать перед тем, как передать значения дат в функцию
consequence_groups
, это преобразовать их в порядковые числа. Затем, используя синтаксис списковых включений list comprehension, мы перебираем группы упорядоченных последовательностей дат, созданных функциейconsecutive_groups
, и конвертируем их обратно в типdatetime.datetime
, используя функцииmap
иfromordinal
.Side_effect
Допустим, вам нужно вызвать побочный эффект при итерации по списку элементов.
Побочный эффект — возможность в процессе выполнения кода: читать и модифицировать значения глобальных переменных или аргументов, осуществлять операции ввода-вывода, реагировать на исключительные ситуации, а также вызывать их обработчики.
В нашем случае этим побочным эффектом может быть запись логов, запись результатов вычислений в файл или, как в приведенном ниже примере, подсчет числа прошедших событий:
import more_itertools num_events = 0 def increment_num_events(_): global num_events num_events += 1 print(f'Всего произошло событий: {num_events}') # создает новый итератор на основе другого range(3) event_iterator = more_itertools.side_effect(increment_num_events, range(3)) more_itertools.consume(event_iterator) #Всего произошло событий: 1 #Всего произошло событий: 2 #Всего произошло событий: 3В примере мы объявляем обычную функцию, которая будет при каждом своем вызове увеличивать на единицу глобальную переменную счетчик
num_events
. Передадим ее в функциюside_effect
вместе с итератором, который создадим с помощью функцииrange
.Далее для того, чтобы перебрать полученный итератор
event_iterator
мы будем использовать функциюconsume
, также предоставляемую модулемmore_itertools
. Она, по сути, запускает и “исчерпывает” полученный итератор, при этом не возвращая значений. По умолчанию используется весь итератор, но может быть предоставлен второй необязательный аргумент для ограничения числа получаемых из него элементов.Позже, когда все элементы
event_iterator
будут выбраны, для каждого будет вызвана функцияincrement_num_events
, выводя в терминале текущее значение счетчика событий, то есть значение переменнойnum_events
.Collapse
Это более мощная версия
flatten
, еще одной функции из модуляmore_itertools
. Функцияcollapse
позволяет сделать плоским список (кортеж) со сколь угодно большим числом уровней вложенности. Можно указать номер уровня до которого нужно «сгладить» исходную последовательность, передав значение для необязательного параметраlevels
. Используя другой необязательный параметрbase_type
можно указать некоторый базовый тип элементов под-последовательности, чтобы остановить «сглаживание» исходной. Вот несколько примеров использования этой функции:import more_itertools import os tree = [40, [25, [10, 3, 17], [32, 30, 38]], [78, 50, 93]] flatten_value = list(more_itertools.collapse(tree)) print(flatten_value) #[40, 25, 10, 3, 17, 32, 30, 38, 78, 50, 93] flatten_value = list(more_itertools.collapse(tree, levels=1)) print(flatten_value) #[40, 25, [10, 3, 17], [32, 30, 38], 78, 50, 93] tree = [40, (25, [10, 3, 17], [32, 30, 38]), [78, 50, 93]] flatten_value = list(more_itertools.collapse(tree, base_type=tuple)) print(flatten_value) #[40, (25, [10, 3, 17], [32, 30, 38]), 78, 50, 93]В этом примере мы обрабатываем древовидную структуру данных в виде вложенных списков и кортежей: сворачиваем ее, чтобы получить плоский список ее значений. В первом случае мы делаем список полностью плоским. Во втором – ограничиваем уровень для «сглаживания». В третьем – указываем тип последовательности, то есть кортеж
tuple
, до уровня которой исходный список будет «сглажен».Split_at
Давайте вернемся к проблеме разбиения последовательностей данных на под-последовательности. Функция
split_at
разбивает итерируемую последовательность на отдельные списки на основе условий, задаваемых предикатом (логическим выражением). Принцип ее работы напоминает функцию split, которая используется для разбиения на части строк по заданному разделителю. Но в нашем случае вместо строки используется итерируемая последовательность, а разделителя – функция предиката.import more_itertools lines = [ "erhgedrgh", "erhgedrghed", "esdrhesdresr", "ktguygkyuk", "-------------", "srdthsrdt", "waefawef", "ryjrtyfj", "-------------", "edthedt", "awefawe", ] list(more_itertools.split_at(lines, lambda x: '-------------' in x)) # [['erhgedrgh', 'erhgedrghed', 'esdrhesdresr', 'ktguygkyuk'], ['srdthsrdt', 'waefawef', 'ryjrtyfj'], ['edthedt', 'awefawe']]В примере выше мы имитируем содержимое текстового файла, полученное при его прочтении, с использованием соответствующего метода, в список строк. Допустим, что этот текстовый файл содержал следующие строки
-------------
, которую мы хотим использовать далее в качестве разделителя. Наличие этой строки мы будем использовать для проверки условия, заданного предикатом, и последующего разделения по ней исходного списка строк. Отметим, что предикат в нашем случае задается лямбда функцией, возвращающей логическое значение с результатом проверки условия. В общем случае в качестве аргумента может передавать любая другая функция возвращающая логическое значение.Bucket
Если вам нужно разделить итерируемую последовательность на несколько отдельных объектов итераторов на основе некоторого условия предиката, то функция
bucket
это именно то, что вам нужно. Она создает дочерние итераторы, группируя исходную последовательность по определенному правилу задаваемому с использованием функции, возвращающей логическое значение. Эту функцию необходимо передать в именованный аргументkey
:# группируем по типу import more_itertools class Cube: pass class Circle: pass class Triangle: pass shapes = [Circle(), Cube(), Circle(), Circle(), Cube(), Triangle(), Triangle()] s = more_itertools.bucket(shapes, key=lambda x: type(x)) # s ->list(s[Cube]) # [<__main__.cube object at>, <__main__.cube object at>] list(s[Circle]) # [<__main__.circle object at>, <__main__.circle object at>, <__main__.circle object at>] В этом примере мы показали, как сгруппировать итерируемую последовательность в зависимости от типа ее элементов (объектов разных классов). Сначала мы объявляем несколько типов (классов) фигур, затем создаем список соответствующих объектов. Далее при вызове функции
bucket
этот список в соответствии с логикой, реализуемой ламбда функцией, преданной вkey
, будет преобразован в объект типаbucket
или корзину с сортированными данными. Этот объект ведет себя как обычный словарьdict
, у которого ключи будут соответствовать именам типов групп, а их значения – итераторами соответствующих значений. Кроме того, как вы можете видеть, каждый элемент в объекте букета является генератором, поэтому нам нужно вызвать функциюlist
, чтобы получить его значения.Map_reduce
Для специалистов по анализу данных эта функция, вероятно, будет самой интересной. В большинстве языков программирования реализованы ее аналоги, так как такой подход к работе с последовательностями данных лежит в основе парадигмы функционального программирования. Поэтому в статье мы не будем останавливаться на подробностях того, как работает
map_reduce
, поскольку об этом уже написано достаточно много. Но я собираюсь показать вам, как ее можно использовать:from more_itertools import map_reduce data = 'This sentence has words of various lengths in it, both short ones and long ones'.split() keyfunc = lambda x: len(x) result = map_reduce(data, keyfunc) # defaultdict(None, { # 4: ['This', 'both', 'ones', 'long', 'ones'], # 8: ['sentence'], # 3: ['has', 'it,', 'and'], # 5: ['words', 'short'], # 2: ['of', 'in'], # 7: ['various', 'lengths']}) valuefunc = lambda x: 1 result = map_reduce(data, keyfunc, valuefunc) # defaultdict(None, { # 4: [1, 1, 1, 1, 1], # 8: [1], # 3: [1, 1, 1], # 5: [1, 1], # 2: [1, 1], # 7: [1, 1]}) reducefunc = sum result = map_reduce(data, keyfunc, valuefunc, reducefunc) # defaultdict(None, { # 4: 5, # 8: 1, # 3: 3, # 5: 2, # 2: 2, # 7: 2})Функция
map_reduce
в качестве значений аргументов может принимать три функции выполняющие следующие операции с элементами, передаваемой в нее последовательности данных.Первый аргумент функции является обязательным. В примере выше функция
keyfunc
используется для категоризации, то есть разбиения элементов исходной последовательности на группы используя определенную в ней логику.Функция, передаваемая во второй необязательный аргумент, в примере это
valuefunc
, используется для преобразования сгруппированных значений в нужную форму (формат).И, наконец, в качестве третьего необязательного аргумента может передаваться еще одна функция. В нашем примере
reducefunc
. Как понятно из имени, она может применяться для обработки сгруппированных элементов, например их суммирования в группе.Sort_together
Если вы работаете с электронными таблицами данных, то вероятно, у вас может возникнуть необходимость сортировать их по значениям, в определенном столбце. Это задача становится простой, если вы используете функцию
sort_together
. Для ее использования нужно лишь указать итерируемую последовательность с данным таблицы и по какому столбцу (столбцам) нужно сортировать данные:# Исходная таблица """ Name | Address | Date of Birth | Updated At ---------------------------------------------------------------- John | | 1994-02-06 | 2020-01-06 Ben | | 1985-04-01 | 2019-03-07 Andy | | 2000-06-25 | 2020-01-08 Mary | | 1998-03-14 | 2018-08-15 """ from more_itertools import sort_together cols = [ ("John", "Ben", "Andy", "Mary"), ("1994-02-06", "1985-04-01", "2000-06-25", "1998-03-14"), ("2020-01-06", "2019-03-07", "2020-01-08", "2018-08-15") ] sort_together(cols, key_list=(1, 2)) # [('Ben', 'John', 'Mary', 'Andy'), ('1985-04-01', '1994-02-06', '1998-03-14', '2000-06-25'), ('2019-03-07', '2020-01-06', '2018-08-15', '2020-01-08')]Исходными данными для функции является итерируемый список
cols
со значениями таблицы по столбцам, как это показано в примере.В параметре
key_list
определяется, какие столбцы таблицы используются для сортировки и с каким приоритетом (в каком порядке проводить сортировку). В случае приведенного этого примера сначала таблица будет сортирована по столбцу с датами рождения Date of Birth, а затем по столбцу со временем обновления данных пользователя Updated At.Seekable
Нам всем нравятся итераторы, например, за их преимущества, позволяющие экономить память и т.д. Но, тем не менее, при работе с ними вы всегда должны быть осторожны, поскольку одна из особенностей их реализации в языке Python заключается в том, что в случае, если вы перебрали все значения предоставляемые итератором, то есть “исчерпали” его, далее работа с ним не возможна. В этом случае вам нужно создавать итератор заново и опять производить итерации по его элементам. Однако вы не обязаны этого делать благодаря функции
seekable
:from more_itertools import seekable data = "This is example sentence for seeking back and forth".split() it = seekable(data) for word in it: ... next(it) # StopIteration it.seek(3) next(it) # "sentence"Функция
seekable
обертывает итерируемую последовательность в объект, который позволяет перемещаться вперед и назад по итератору, даже после того, как всего элементы были получены. В примере выше вы можете видеть, что после выборки всех элементов итератора у нас возникло исключение типаStopIteration
. Но мы можем вернуться или, так сказать, “перемотать” указатель текущего элемента назад, а затем продолжить работу.Filter_except
Взгляните на следующий пример кода. Допустим, вы получили итерируемую последовательность данных, в нашем случае список строк. Строковое содержимое ее элементов смешанное, то есть они содержат как текст, так и числа. Однако вы хотите выделить из нее только числа, которые могут быть целыми, с плавающей запятой или в экспоненциальной форме.
from more_itertools import filter_except data = ['1.5', '6', 'not-important', '11', '1.23E-7', 'remove-me', '25', 'trash'] list(map(float, filter_except(float, data, TypeError, ValueError))) # [1.5, 6.0, 11.0, 1.23e-07, 25.0]Функция
filter_except
фильтрует элементы исходной итерируемой последовательности, последовательно передавая ее элементы в заданную функцию (float
). Функция проверяет, возбуждается при ее вызове исключение заданного типа (TypeError, ValueError
) или нет, сохраняя только элементы, прошедшие проверку.Unique_to_each
unique_to_each
– одна из самых непонятных функций в модулеmore_itertools
. Она принимает на вход несколько итерируемых последовательностей и возвращает элементы из каждой из них, которых нет в других, то есть уникальные элементы. Посмотрим на нее в действии на примере:from more_itertools import unique_to_each # Граф (список смежных вершин) graph = {'A': {'B', 'E'}, 'B': {'A', 'C'}, 'C': {'B'}, 'D': {'E'}, 'E': {'A', 'D'}} unique_to_each({'B', 'E'}, {'A', 'C'}, {'B'}, {'E'}, {'A', 'D'}) # [[], ['C'], [], [], ['D']] # Если мы отбрасываем узел B, то C становится изолированным, а если мы отбрасываем узел E, то и D становится изолированнымВ примере выше мы определяем структуру графа данных, используя список смежных вершин adjacency list (фактически словарь dict). Затем передаем последовательности вершин соседей каждого узла как отдельный набор данных
unique_to_each
. Наша функция выведет список узлов, которые будут изолированы, если соответствующий узел будет удален.Numeric_range
Довольно часто нам приходится иметь дело с генерацией последовательностей нецелочисленных значений в заданном диапазоне. Кроме того получаемые значения могут представляться в форматах, отличных от простых типов. И в этом случае функция
numeric_range
именно то, что нам нужно:from more_itertools import numeric_range import datetime from decimal import Decimal list(numeric_range(Decimal('1.7'), Decimal('3.5'), Decimal('0.3'))) # [Decimal('1.7'), Decimal('2.0'), Decimal('2.3'), Decimal('2.6'), Decimal('2.9'), Decimal('3.2')] start = datetime.datetime(2020, 2, 10) stop = datetime.datetime(2020, 2, 15) step = datetime.timedelta(days=2) list(numeric_range(start, stop, step)) # [datetime.datetime(2020, 2, 10, 0, 0), datetime.datetime(2020, 2, 12, 0, 0), datetime.datetime(2020, 2, 14, 0, 0)]Самое приятное в использовании функции
numeric_range
то, что она ведет себя точно так же, как функцияrange
. То есть для управления ее работой необходимо указать значения тех же аргументов:start
,stop
иstep
. В примере выше мы сначала получаем генератор значений типа Decimal, то есть десятичных дробей от 1.7 до 3.5 с шагом 0.3, а затем генератор дат datetime между 2020/2/10 и 2020/2/15 с шагом 2 дня.Make_decorator
В заключение рассмотрим функцию
make_decorator
. Она позволяет использовать другие функции, использующиеся для обработки итерируемых последовательностей, для создания декораторов. Основным ее преимуществом, является то, что кроме преобразования выходных данных декорируемых функций, она позволяет создавать на их основе новые итераторы:from more_itertools import make_decorator from more_itertools import map_except mapper_except = make_decorator(map_except, result_index=1) @mapper_except(float, ValueError, TypeError) def read_file(f): ... # Считываем из последовательность строк в виде текстовых и числовых данных return ['1.5', '6', 'not-important', '11', '1.23E-7', 'remove-me', '25', 'trash'] list(read_file("file.txt")) # [1.5, 6.0, 11.0, 1.23e-07, 25.0]В этом примере на основе функции
map_except
вначале создаем декоратор. Отметим, что при вызовеmake_decorator
в именованный аргументresult_index = 1
мы передаем значение, соответствующее позиции аргумента функцииmap_except
, в который передается итерируемая последовательность. Функцияread_file
имитирует чтение списка строк из некоторого файла, а затем возвращает итератор списка строк. Результат работы внутренней функции передается декоратору, который фильтрует последовательность строк, убирая ненужные элементы, оставляя только строки, представляющие собой числа с плавающей запятой.Заключение
Я надеюсь, что вы узнали что-то новое из этой статьи. Я уверен что в дальнейшем использование возможностей модулей
itertools
иmore_itertools
поможет значительно облегчить вашу жизнь. Тем не менее использование рассмотренных модулей требует некоторой практики. Кроме примеров рассмотренных в этой статье вы можете ознакомиться с рецептами itertools или просто заставьте себя использовать их как можно чаще, чтобы освоить основы работы с ними. 😉
Спасибо.