Python предоставляет замечательный модуль для создания собственных итераторов. Я говорю о модуле itertools. Предоставленные данным модулем инструменты быстрые и эффективно используют память. Вы сможете использовать эти кирпичики для создания собственных специализированных итераторов, которые могут использоваться для эффективных циклов. В данной статье мы рассмотрим примеры каждого предоставленного инструмента, так что к её концу вы сможете эффективно использовать каждый инструмент для создания собственных скриптов, а главное, знать, когда именно их применять.
Бесконечные итераторы
Пакет itertools содержит три итератора, которые могут выполнять итерацию бесконечно. Это означает, что когда вы используете их, вам нужно прекрасно понимать, что в конечном итоге вам нужно разорвать эти итераторы, во избежание бесконечного цикла. Это может быть применимо при генерации чисел или при циклическом переключении по итерациям неизвестной длины, например. Давайте начнем с изучения этих итераций!
count(start=0, step=1)
Итератор count возвращает равномерно распределенные переменные, начиная с числа, которое вы передаете в качестве стартового параметра. Он также принимает параметр шага. Давайте взглянем на простой пример:
1 2 3 4 5 6 7 |
from itertools import count for i in count(10): if i > 20: break else: print(i) |
Результат:
1 2 3 4 5 6 7 8 9 10 11 |
10 11 12 13 14 15 16 17 18 19 20 |
Мы импортируем count из itertools и создаем цикл for. Мы добавляем условную проверку, которая разорвет цикл, когда итератор превысит отметку 20, в противном случае он выводит, где именно мы находимся в итераторе. Обратите внимание на то, что результат начинается с 10, так как мы именно это и указали в качестве нашего стартового значения. Еще один способ ограничить выдачу данного бесконечного итератора, это использовать наследуемый модуль из itertools, под названием islice. Как это работает:
1 2 3 4 |
from itertools import islice for i in islice(count(10), 5): print(i) |
Результат:
1 2 3 4 5 |
10 11 12 13 14 |
В этом примере мы импортируем islice и зацикливаем по count, начиная с 10 и заканчивая после 5 элементов. Как вы могли догадаться, второй аргумент «когда» нужен для остановки итерации. Но это не означает «остановись, когда я дойду до числа 5», вместо этого «остановись, когда мы достигнем пяти итераций».
cycle(iterable)
Итератор cycle из itertools позволяет вам создавать итератор, которой создает бесконечный цикл ряда значений. Давайте передадим ему строку из трех букв и посмотрим, к чему это приведет:
1 2 3 4 5 6 7 8 |
from itertools import cycle count = 0 for item in cycle('XYZ'): if count > 7: break print(item) count += 1 |
Результат:
1 2 3 4 5 6 7 8 |
X Y Z X Y Z X Y |
Здесь мы создаем цикл for для того, чтобы бесконечно зациклить буквы Z, Y, Z. Конечно, нам не нужен бесконечный цикл на постоянной основе, так что мы добавим простой счетчик для разрыва цикла. Вы также можете использовать встроенный инструмент Python под названием next для итерации над итераторами, которые вы создаете при помощи itertools:
1 2 3 4 5 6 7 8 9 10 11 |
from itertools import cycle polys = ['triangle', 'square', 'pentagon', 'rectangle'] iterator = cycle(polys) print(next(iterator)) # triangle print(next(iterator)) # square print(next(iterator)) # pentagon print(next(iterator)) # rectangle print(next(iterator)) # triangle print(next(iterator)) # square |
В данном коде мы создаем простой список полигонов и передаем их циклу. Мы сохраняем наш новый итератор в качестве переменной, и затем мы передаем эту переменную нашей следующей функции. Каждый раз, когда мы вызываем функцию, она возвращает следующее значение в итераторе. Так как этот итератор бесконечный, мы можем вызывать next днями напролет, и все равно объекты никогда не закончатся.
repeat(object)
Итератор возвращает объект снова и снова, бесконечно, пока вы не настроите его аргумент times. Это очень похоже на cycle, за исключением того, что он не зацикливает набор значений повторно. Давайте взглянем на простой пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
from itertools import repeat iterator = repeat('test', 5) print(next(iterator)) # test print(next(iterator)) # test print(next(iterator)) # test print(next(iterator)) # test print(next(iterator)) # test # В 6й раз будет ошибка... print(next(iterator)) Traceback (most recent call last): Python Shell, prompt 21, line 1 builtins.StopIteration: |
Здесь мы импортируем repeat и указываем ему повторить текст «test» пять раз. Далее мы вызываем next в нашем новом итераторе шесть раз, что бы увидеть, все ли работает правильно. Когда вы запустите этот код, вы увидите, что появится ошибка StopIteration, так как мы превысили количество значений в нашем итераторе
Есть вопросы по Python?
На нашем форуме вы можете задать любой вопрос и получить ответ от всего нашего сообщества!
Паблик VK
Одно из самых больших сообществ по Python в социальной сети ВК. Видео уроки и книги для вас!
Конечные итераторы
Большая часть итераторов, которые вы создаете при помощи itertools, не являются бесконечными. В этом разделе, мы изучим конечные итераторы itertools. Для получения читабельной выдачи, мы используем встроенный тип списка Python. Если вы не используете список, вы получите только объект itertools в выдаче.
accumulate(iterable)
Итератор accumulate возвращает накопленные суммы или накопленные результаты двух аргументных функций, которые вы передали для накопления. По умолчанию, accumulate – это дополнение, так что давайте посмотрим, как она работает на деле:
1 2 3 4 |
from itertools import accumulate result = list(accumulate(range(10))) print(result) |
Результат использования range:
1 |
[0, 1, 3, 6, 10, 15, 21, 28, 36, 45] |
Здесь мы импортируем accumulate и передаем ей ряд из 10 чисел от 0 до 9. Он добавляет каждое из них по очереди, начиная с 0, затем 0+1, затем 1+2, и так далее. Давайте импортируем модуль operator и добавим его в смесь:
1 2 3 4 5 |
from itertools import accumulate import operator result = list(accumulate(range(1, 5), operator.mul)) print(result) # [1, 2, 6, 24] |
Здесь мы передали числа от 1 до 4 в наш итератор accumulate. Мы также передаем ему функцию operator.mul. Эти функции принимают аргументы, для возможности умножения. Так что в каждой итерации она не суммирует, а умножает (1×1=1, 1×2=2, 2×3=6, и т.д.). Документация accumulate показывает несколько других интересных примеров, таких как амортизация и хаотичное рекуррентное соотношение. Вам определенно следует изучить эти примеры, так как они могут оказаться весьма полезными на практике.
chain(*iterables)
Итератор chain берет ряд итерируемых, и лепит из них одну длинную итерируемую. Недавно это очень выручило меня во время работы над одним проектом. В общем, у нас есть список с определенными объектами, и два других списка, которые нам нужно добавить в общий список, но нам нужно только присоединить объекты каждого списка с объектами общего списка, а не создавать список списков. Изначально я попробовал сделать как-то так:
1 2 3 4 5 6 7 8 |
my_list = ['foo', 'bar'] numbers = list(range(5)) cmd = ['ls', '/some/dir'] my_list.extend(cmd, numbers) print(my_list) # ['foo', 'bar', ['ls', '/some/dir'], [0, 1, 2, 3, 4]] |
К сожалению, это не сработало так, как я ожидал. Модуль itertools предоставляет более элегантный способ совмещения этих списков в один при помощи chain:
1 2 3 4 5 6 7 8 9 |
from itertools import chain numbers = list(range(5)) cmd = ['ls', '/some/dir'] my_list = list(chain(['foo', 'bar'], cmd, numbers)) print(my_list) # ['foo', 'bar', 'ls', '/some/dir', 0, 1, 2, 3, 4] |
Самые проницательные мои читатели могут заметить, что здесь виден другой способ, которым можно достичь той же цели без использования itertools. Вы можете сделать следующее, для получения аналогичного результата:
1 2 3 4 5 |
my_list = ['foo', 'bar'] my_list += cmd + numbers print(my_list) # ['foo', 'bar', 'ls', '/some/dir', 0, 1, 2, 3, 4] |
Оба этих метода определенно работают, и до того, как я узнал о chain, я бы выбрал второе направление, но я думаю, что chain более прямой и простой способ понимания решения данного случая.
chain.from_iterable(iterable)
Вы также можете использовать метод chain под названием from_iterable. Этот метод работает немного иначе, не используя chain напрямую. Вместо передачи ряда итерируемых вам нужно передать вложенный список. Давайте посмотрим:
1 2 3 4 5 6 7 |
from itertools import chain numbers = list(range(5)) cmd = ['ls', '/some/dir'] data = list(chain.from_iterable([cmd, numbers])) print(data) # ['ls', '/some/dir', 0, 1, 2, 3, 4] |
compress(data, selectors)
Наследуемый модуль compress полезен при фильтрации первой итерируемой со второй. Это работает путем превращения второй итерируемой в список с Boolean (или с единицами и нулями, что одно и тоже). Как это работает:
1 2 3 4 5 6 7 |
from itertools import compress letters = 'ABCDEFG' bools = [True, False, True, True, False] data = list(compress(letters, bools)) print(data) # ['A', 'C', 'D'] |
В данном примере у нас есть группа из семи букв и список из True и False. Далее мы передаем их функции compress. Функция compress пройдете через каждую соответствующую итерируемую, и сравнит первую со второй. Если вторая получает оценку True, то значит что с ней все хорошо. Если False, то этот объект будет удален. Таким образом, если вы изучите приведенный выше пример, вы увидите, что у нас True на первой, третьей и четвертой позициях, что соответствует A,C и D.
dropwhile(predicate, iterable)
У нас в распоряжении имеется небольшой итератор itertools под названием dropwhile. Этот малыш может удалять элементы, если критерием фильтра является True. По этой причине, вы можете не увидеть никакой выдачи из этого итератора, пока предикат не станет False. Это может затянуть время запуска, а нам этого не нужно. Давайте посмотрим на пример из документации Python.
1 2 3 4 |
from itertools import dropwhile data = list(dropwhile(lambda x: x<5, [1,4,6,4,1])) print(data) # [6, 4, 1] |
Здесь мы импортируем dropwhile, затем передаем его простому оператору lambda. Эта функция выдает True, если значение х меньше или равно 5. В противном случае она вернет False. Функция dropwhile создаст цикл над списком и передаст каждый элемент лямбде. Если лямбда возвращает True, тогда значение будет удалено. Как только мы достигнем цифры 6, лямбда вернет False, и мы сохраняем число 6 и все значения, которые за ним следуют. Я нашел это весьма полезным в применении обычной функции над лямбдой, когда я исследую что-нибудь новое. Так что давайте перевернем ситуацию с ног на голову и создадим функцию, которая возвращает True, если значение превышает число 5.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
from itertools import dropwhile def greater_than_five(x): return x > 5 data = list( dropwhile( greater_than_five, [6, 7, 8, 9, 1, 2, 3, 10] ) ) print(data) # [1, 2, 3, 10] |
Здесь мы создали простую функцию в интерпретаторе Python. Это функция является нашим предикатом (или фильтром). Если переданные нами значения отмечены как True, то они будут удалены. Как только мы достигнем значения меньше 5-и, то ВСЕ значения после и включая это значение сохранятся, что мы и видим в примере выше.
filterfalse(predicate, iterable)
Функция itertools под названием filterfalse очень похожа на dropwhile. Однако, вместо сброса значений, отмеченных как True, filterfalse только вернет те значения, которые оцениваются как False. Давайте используем нашу функцию из предыдущего раздела:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
from itertools import filterfalse def greater_than_five(x): return x > 5 data = list( filterfalse( greater_than_five, [6, 7, 8, 9, 1, 2, 3, 10] ) ) print(data) # [1, 2, 3] |
Здесь мы передаем filterfalse нашей функции и список чисел. Если число меньше пяти, оно сохраняется. В противном случае, оно отбрасывается. Вы заметите, что наш результат только 1, 2 и 3. В отличие от dropwhile, filterfalse проверит каждое значение в фильтре.
groupby(iterable, key=None)
Итератор groupby возвращает последовательные ключи и группы из итерируемой. Это трудно понять, не взглянув на пример. Но мы это исправим! Введите следующий код в ваш интерпретатор или сохраните его в файле:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
from itertools import groupby vehicles = [('Ford', 'Taurus'), ('Dodge', 'Durango'), ('Chevrolet', 'Cobalt'), ('Ford', 'F150'), ('Dodge', 'Charger'), ('Ford', 'GT')] sorted_vehicles = sorted(vehicles) for key, group in groupby(sorted_vehicles, lambda make: make[0]): for make, model in group: print('{model} is made by {make}'.format(model=model, make=make)) print ("**** END OF GROUP ***\n") |
Здесь мы импортируем groupby и создаем список кортежей. Далее мы сортируем данные, для корректного вывода, а также, чтобы позволить groupby группировать объекты корректным образом. Далее, мы зацикливаем выданный groupby итератор, который дает нам ключ и группу. Затем мы зацикливаем группу и выводим то, что в ней. Если вы запустите этот код, вы увидите что-то вроде следующего:
1 2 3 4 5 6 7 8 9 10 11 |
Cobalt is made by Chevrolet **** END OF GROUP *** Charger is made by Dodge Durango is made by Dodge **** END OF GROUP *** F150 is made by Ford GT is made by Ford Taurus is made by Ford **** END OF GROUP *** |
Ради смеха, попробуйте изменить код, передав vehicles вместо sorted_vehicles. Так вы сразу поймете, зачем нужно сортировать данные перед их запуском в groupby.
islice(iterable, start, stop)
Ранее мы упоминали islice в разделе count. Давайте немного углубимся в данный вопрос. Итератор islice возвращает указанные элементы из итерируемой. Это своего рода непрозрачный оператор. В целом, islice использует срез индекса вашей итерируемой (тот или иной объект, над которым вы выполняете итерацию) и возвращает выбранный объект в качестве итератора. Существует две реализации islice.
itertools.islice(iterable, stop) и новая версия islice, которая более соответствует обычному слайсингу Python: islice(iterable, start, stop[, step]). Давайте рассмотрим первую версию, чтобы понять, как это работает:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
from itertools import islice iterator = islice('123456', 4) print(next(iterator)) # 1 print(next(iterator)) # 2 print(next(iterator)) # 3 print(next(iterator)) # 4 print(next(iterator)) # ошибка! Traceback (most recent call last): Python Shell, prompt 15, line 1 builtins.StopIteration: |
В данном коде мы передаем строку из шести символов нашему islice совместно с числом 4, который является стоп-аргументом. Это значит, что итератор, возвращаемый islice, будет содержать первые 4 объекта в содержащейся в нем строке. Мы можем проверить это, вызвав next в нашем итераторе четыре раза, что мы и делали выше. Python достаточно продуманный, чтобы понять, что если мы имеем только два переданных islice аргумента, то второй аргумент является стоп-аргументом. Давайте попробуем передать ему три аргумента, чтобы продемонстрировать, что вы можете передать ему стартовый и стоп аргументы. Инструмент itertools под названием count поможет нам продемонстрировать данную задумку:
1 2 3 4 5 |
from itertools import islice from itertools import count for i in islice(count(), 3, 15): print(i) |
Результат:
1 2 3 4 5 6 7 8 9 10 11 12 |
3 4 5 6 7 8 9 10 11 12 13 14 |
Здесь мы вызвали count и указали islice, что мы начинаем с числа 3 и заканчиваем тогда, когда достигнем числа 15. Это очень похоже на слайсинг, за исключением того, что мы делаем это с итератором и получаем новый итератор!
starmap(function, iterable)
Инструмент starmap создает итератор, который может проводить вычисления, используя функцию и итерируемый. В документации отмечают следующее:
Разница между map() и starmap() параллельна различию между функцией (a,b) и функцией (*с).
Давайте взглянем на простой пример:
1 2 3 4 5 6 7 |
from itertools import starmap def add(a, b): return a+b for item in starmap(add, [(2,3), (4,5)]): print(item) |
Здесь мы создаем простую функцию добавления, которая принимает два аргумента. Далее мы создаем цикл for и вызываем starmap с функцию в его первом аргументе и списком кортежей для итерируемой. Функция starmap передает все объекты кортежей функции и возвращает итератор результатов, который мы выводим.
takewhile(predicate, iterable)
Модуль takewhile это как рассматриваемый нами dropwhile, только наоборот. Модуль takewhile создает итератор, который возвращает элементы из итерируемой до тех пор, пока наш предикат или фильтр оцениваются как True. Давайте рассмотрим небольшой пример, чтобы разобраться с тем, как это реализуется:
1 2 3 4 |
from itertools import takewhile data = list(takewhile(lambda x: x<5, [1,4,6,4,1])) print(data) # [1, 4] |
Здесь мы запускаем takewhile при помощи функции лямбда и списка. Выдача представлена только первыми двумя целыми числами нашей итерируемой. Причина в том, что и 1 и 4 меньше, чем 5, но 6 имеет большее значение. Так что после того, как takewhile столкнется с цифрой 6, условие станет False, и дальнейшие объекты итерируемой будут игнорироваться.
tee(iterable, n=2)
Инструмент tee создает n количество итераторов из одной итерируемой. Это значит, что вы можете создать несколько итераторов из одной итерируемой. Давайте взглянем на следующий код, чтобы понять, что к чему:
1 2 3 4 5 6 7 |
from itertools import tee data = 'ABCDE' iter1, iter2 = tee(data) for item in iter1: print(item) |
Результат:
1 2 3 4 5 |
A B C D E |
1 2 |
for item in iter2: print(item) |
Результат:
1 2 3 4 5 |
A B C D E |
Здесь мы создаем строку из 5 букв и передаем её tee. Так как tee по умолчанию равен 2, мы используем множественное присваивание для получения двух итераторов, которые вернет tee. Далее, мы зацикливаем каждый итератор и выводим их содержимое. Как мы видим, их содержимое одинаково.
zip_longest(*iterables, fillvalue=None)
Итератор zip_longest может быть использован для сжатия двух итерируемых вместе. Если так вышло, что у итерируемых разная длина, тогда вы можете передать их fillvalue. Давайте взглянем на простой пример, который основан на документации данной функции:
1 2 3 4 |
from itertools import zip_longest for item in zip_longest('ABCD', 'xy', fillvalue='BLANK'): print (item) |
Результат:
1 2 3 4 |
('A', 'x') ('B', 'y') ('C', 'BLANK') ('D', 'BLANK') |
В этом коде мы импортируем zip_longest, затем передаем ему две строки для совместного сжатия. Вы заметите, что длина первой строки составляет 4 символа, в то время как длина второй – всего 2 символа. Мы также устанавливаем значение заполнения как BLANK. После зацикливания и вывода, вы увидите, что выдача содержит кортежи. Первые два кортежа являются комбинацией первой и второй буквы каждый строки соответственно. Последние две буквы содержат наше значение заполнения. Обратите внимание на то, что если iterable(s) передан zip_longest, то он может быть бесконечным. Так что вам может понадобиться завернуть функцию во что-нибудь вроде islice для ограничения количества вызовов.
Комбинаторные генераторы
Библиотека itertools содержит четыре итератора, которые могут быть использованы для создания комбинаций и перестановки данных. Мы рассмотрим эти итераторы в данном разделе.
combinations(iterable, r)
Если вам нужно создать комбинации, Python предоставляет вам itertools.combinations. Давайте взглянем на пример:
1 2 3 4 |
from itertools import combinations data = list(combinations('WXYZ', 2)) print(data) |
Результат:
1 2 3 4 5 6 7 8 |
[ ('W', 'X'), ('W', 'Y'), ('W', 'Z'), ('X', 'Y'), ('X', 'Z'), ('Y', 'Z') ] |
Запустив этот код вы заметите, что combinations возвращает кортежи. Чтобы сделать выдачу более-менее читаемой, давайте зациклим наш итератор и разместим кортежи в одной строке:
1 2 3 4 |
from itertools import combinations for item in combinations('WXYZ', 2): print(''.join(item)) |
Результат:
1 2 3 4 5 6 |
WX WY WZ XY XZ YZ |
Так-то намного проще разобрать различные комбинации. Обратите внимание, что функция combinations делает свои комбинации в лексикографическом порядке, так что если итерируемая отсортирована, тогда ваши кортежи комбинаций также будут отсортированы. Также стоит отметить, что combinations не будет давать повторяемые значения, если все эти значения являются уникальными.
combinations_with_replacement(iterable, r)
Итератор под названием combinations_with_replacement очень похож на combinations. Единственная разница в том, что он создает комбинации повторяемых элементов. Давайте посмотрим на пример из предыдущего раздела:
1 2 3 4 |
from itertools import combinations_with_replacement for item in combinations_with_replacement('WXYZ', 2): print(''.join(item)) |
Результат:
1 2 3 4 5 6 7 8 9 10 |
WW WX WY WZ XX XY XZ YY YZ ZZ |
Как вы видите, у нас есть четыре объекта в выдаче: WW, XX, YY и ZZ.
product(*iterables, repeat=1)
Пакет itertools содержит небольшую, но очень полезную функцию, которая создает продукты Cartesian из ряда вложенных итерируемых. Да, эта функция является продуктом. Давайте посмотрим на то, как это работает!
1 2 3 4 5 6 |
from itertools import product arrays = [(-1,1), (-3,3), (-5,5)] cp = list(product(*arrays)) print(cp) |
Результат:
1 2 3 4 5 6 7 8 |
[(-1, -3, -5), (-1, -3, 5), (-1, 3, -5), (-1, 3, 5), (1, -3, -5), (1, -3, 5), (1, 3, -5), (1, 3, 5)] |
Здесь мы импортируем product, затем устанавливаем список кортежей, который мы назначаем переменным массивам. Далее, мы вызываем product с этими массивами. Вы заметите, что мы вызываем их при помощи *arrays. Благодаря этому список будет «взорван» или применен к функции продукта в определенной последовательности. Это значит, что вы передаете 3 аргумента, вместо одного. Если хотите, попробуйте вызвать её, используя предварительно скопированную в массивы звездочку.
permutations
Наследуемый модуль itertools под названием permutations возвращает последовательные перестановки элементов из итерируемого вами значения той или иной длины. Как и функция combinations, permutations создает выдачу в лексикографическом порядке сортировки. Давайте посмотрим на пример:
1 2 3 4 |
from itertools import permutations for item in permutations('WXYZ', 2): print(''.join(item)) |
Результат:
1 2 3 4 5 6 7 8 9 10 11 12 |
WX WY WZ XW XY XZ YW YX YZ ZW ZX ZY |
Обратите внимание на то, что выдача несколько длиннее, чем у функции combinations. Когда вы используете permutations, он перепробует все варианты перестановок в строке, но не будет повторять значения, если вложенные элементы являются уникальными
Подведем итоги
Модуль itertools – это очень разносторонний набор инструментов для создания итераторов. Вы можете использовать их для создания собственных итераторов по отдельности, или комбинировать уже существующие. Документация Python содержит множество замечательных примеров, которые рекомендуется изучить, чтобы понять, что и как можно делать с этой ценнейшей библиотекой.
Являюсь администратором нескольких порталов по обучению языков программирования Python, Golang и Kotlin. В составе небольшой команды единомышленников, мы занимаемся популяризацией языков программирования на русскоязычную аудиторию. Большая часть статей была адаптирована нами на русский язык и распространяется бесплатно.
E-mail: vasile.buldumac@ati.utm.md
Образование
Universitatea Tehnică a Moldovei (utm.md)
- 2014 — 2018 Технический Университет Молдовы, ИТ-Инженер. Тема дипломной работы «Автоматизация покупки и продажи криптовалюты используя технический анализ»
- 2018 — 2020 Технический Университет Молдовы, Магистр, Магистерская диссертация «Идентификация человека в киберпространстве по фотографии лица»