Що таке функції Python Itertools?

Згідно з офіційною документацією Python, модуль `itertools` надає розробникам широкий спектр ефективних та швидких інструментів для роботи з ітераторами. Ці інструменти можна використовувати як окремо, так і комбінувати для створення складних ітерацій, що дозволяє обробляти великі обсяги даних зі значною економією пам’яті.

Модуль `itertools` включає функції, що полегшують маніпуляції з ітераторами, особливо при роботі з великими наборами даних. Ці функції можуть використовуватись для перетворення існуючих ітераторів на більш складні структури.

Крім того, використання `itertools` допомагає зменшити кількість помилок під час обробки ітерацій, сприяючи написанню більш зрозумілого, легкого для читання та підтримки коду.

Функції, що надаються модулем `itertools`, можна розподілити на три основні категорії:

#1. Нескінченні ітератори

Ці ітератори призначені для роботи з нескінченними послідовностями. Їх використання може призвести до нескінченного циклу, якщо не передбачена умова його завершення. Вони корисні при моделюванні безперервних процесів або генерації необмеженої кількості значень. Модуль `itertools` надає три нескінченні ітератори: `count()`, `cycle()`, і `repeat()`.

#2. Комбінаторні ітератори

Комбінаторні ітератори включають функції для виконання операцій, таких як декартовий добуток, комбінації та перестановки елементів ітератора. Ці інструменти є незамінними при пошуку всіх можливих варіантів впорядкування або об’єднання елементів. Модуль `itertools` надає чотири комбінаторні ітератори: `product()`, `permutations()`, `combinations()` і `combinations_with_replacement()`.

#3. Ітератори, що завершуються на найкоротшій вхідній послідовності

Ці ітератори є кінцевими і застосовуються до кінцевих послідовностей. Вони генерують вихідні дані на основі типу обраної функції. До цієї категорії належать: `accumulate()`, `chain()`, `chain.from_iterable()`, `compress()`, `dropwhile()`, `filterfalse()`, `groupby()`, `islice()`, `pairwise()`, `starmap()`, `takewhile()`, `tee()` та `zip_longest()`.

Розглянемо, як працюють різні функції `itertools` залежно від їхньої категорії:

Нескінченні ітератори

До нескінченних ітераторів належать:

#1. `count()`

Функція `count(start, step)` створює нескінченну послідовність чисел, починаючи з початкового значення `start`. Вона приймає два необов’язкових аргументи: `start` і `step`. Аргумент `start` визначає, з якого числа починається послідовність, і за замовчуванням дорівнює 0, якщо не вказано інше. Аргумент `step` задає різницю між кожним наступним числом. За замовчуванням `step` дорівнює 1.

import itertools
# Починаємо відлік з 4, крок 2
for i in itertools.count(4, 2):
    # Умова для завершення циклу
    if i == 14:
        break
    else:
        print(i) # Вивід - 4, 6, 8, 10, 12

Вивід:

4
6
8
10
12

#2. `cycle()`

Функція `cycle(iterable)` приймає ітератор як аргумент та повертає ітератор, що нескінченно перебирає елементи заданої послідовності. Коли послідовність вичерпується, цикл починається спочатку.

Наприклад, якщо передати список `[“red”, “green”, “yellow”]` в `cycle()`, то в першому циклі ми отримаємо “red”, в другому – “green”, в третьому – “yellow”. В четвертому циклі знову почнемо з “red” і так далі.

Важливо зберегти результат `cycle()` у змінну, щоб ітератор зберігав свій стан і не починав з початку кожного разу.

import itertools

colors = ["red", "green", "yellow"]
# Передаємо список кольорів до cycle()
color_cycle = itertools.cycle(colors)
print(color_cycle)

# Використовуємо range для обмеження виводу 7 ітераціями
# next() для отримання наступного елемента з ітератора
for i in range(7):
    print(next(color_cycle))

Вивід:

red
green
yellow
red
green
yellow
red

#3. `repeat()`

`repeat(elem, n)` приймає два аргументи: елемент `elem`, який потрібно повторювати, та кількість разів `n`. Елементом може бути як окреме значення, так і ітерований об’єкт. Якщо `n` не вказано, елемент буде повторюватися нескінченно.

import itertools
   
for i in itertools.repeat(10, 3):
    print(i)

Вивід:

10
10
10

Комбінаторні ітератори

Комбінаторні ітератори включають:

#1. `product()`

`product()` — це функція для обчислення декартового добутку ітераторів. Якщо є дві ітерації, наприклад `x = {7, 8}` та `y = {1, 2, 3}`, їх декартовий добуток складатиметься з усіх можливих комбінацій, де перший елемент з `x`, а другий з `y`. У цьому випадку результат буде `[(7, 1), (7, 2), (7, 3), (8, 1), (8, 2), (8, 3)]`.

`product()` приймає необов’язковий параметр `repeat`, який використовується для обчислення декартового добутку ітератора з самим собою. Значення `repeat` визначає кількість повторень кожного елемента з вхідних ітерацій під час обчислення добутку.

Наприклад, виклик `product(‘ABCD’, repeat=2)` поверне комбінації, як-от `(‘A’, ‘A’)`, `(‘A’, ‘B’)`, `(‘A’, ‘C’)` і так далі. Якщо `repeat` буде 3, то результат матиме вигляд `(‘A’, ‘A’, ‘A’)`, `(‘A’, ‘A’, ‘B’)`, `(‘A’, ‘A’, ‘C’)`, `(‘A’, ‘A’, ‘D’)` і тому подібне.

from itertools import product
# product() з необов'язковим аргументом repeat
print("product() з необов'язковим аргументом repeat ")
print(list(product('ABC', repeat = 2)))

# product без repeat
print("product() БЕЗ необов'язкового аргументу repeat")
print(list(product([7,8], [1,2,3])))

Вивід

product() з необов'язковим аргументом repeat 
[('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'B'), ('B', 'C'), ('C', 'A'), ('C', 'B'), ('C', 'C')]
product() БЕЗ необов'язкового аргументу repeat
[(7, 1), (7, 2), (7, 3), (8, 1), (8, 2), (8, 3)]

#2. `permutations()`

`permutations(iterable, group_size)` повертає всі можливі перестановки елементів із заданого ітератора. Перестановка – це спосіб упорядкування елементів. Функція приймає необов’язковий аргумент `group_size`. Якщо `group_size` не вказано, то згенеровані перестановки матимуть таку ж довжину, як і сам ітератор.

import itertools
numbers = [1, 2, 3]
sized_permutations = list(itertools.permutations(numbers,2))
unsized_permuatations = list(itertools.permutations(numbers))

print("Перестановки з розміром 2")
print(sized_permutations)
print("Перестановки без аргументу розміру")
print(unsized_permuatations)

Вивід

Перестановки з розміром 2
[(1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)]
Перестановки без аргументу розміру
[(1, 2, 3), (1, 3, 2), (2, 1, 3), (2, 3, 1), (3, 1, 2), (3, 2, 1)]

#3. `combinations()`

`combinations(iterable, size)` повертає всі можливі комбінації елементів заданого розміру з ітератора. Аргумент `size` визначає розмір кожної комбінації.

Результати впорядковані. Комбінація відрізняється від перестановки тим, що в перестановці важливий порядок, а в комбінації – ні. Наприклад, в `[A, B, C]` є 6 перестановок: AB, AC, BA, BC, CA, CB, але лише 3 комбінації: AB, AC, BC.

import itertools
numbers = [1, 2, 3,4]
size2_combination = list(itertools.combinations(numbers,2))
size3_combination = list(itertools.combinations(numbers, 3))

print("Комбінації з розміром 2")
print(size2_combination)
print("Комбінації з розміром 3")
print(size3_combination)

Вивід:

Комбінації з розміром 2
[(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
Комбінації з розміром 3
[(1, 2, 3), (1, 2, 4), (1, 3, 4), (2, 3, 4)]

#4. `combinations_with_replacement()`

`combinations_with_replacement(iterable, size)` генерує всі можливі комбінації заданої довжини з ітератора, дозволяючи повторювати елементи у вихідних комбінаціях. `size` визначає розмір згенерованих комбінацій.

Ця функція відрізняється від `combinations()` тим, що повертає комбінації, де елемент може зустрічатися більше одного разу. Наприклад, можна отримати комбінацію, як `(1, 1)`, яку неможливо отримати за допомогою `combinations()`.

import itertools
numbers = [1, 2, 3,4]

size2_combination = list(itertools.combinations_with_replacement(numbers,2))
print("Combinations_with_replacement => розмір 2")
print(size2_combination)

Вивід

Combinations_with_replacement => розмір 2
[(1, 1), (1, 2), (1, 3), (1, 4), (2, 2), (2, 3), (2, 4), (3, 3), (3, 4), (4, 4)]

Кінцеві ітератори

Ці ітератори включають такі функції, як:

#1. `accumulate()`

`accumulate(iterable, function)` приймає ітератор та необов’язковий аргумент – функцію. Він повертає накопичений результат застосування функції до елементів ітератора. Якщо функцію не вказано, то за замовчуванням використовується додавання.

import itertools
import operator
numbers = [1, 2, 3, 4, 5]

# Акумуляція суми чисел
accumulated_val = itertools.accumulate(numbers)
accumulated_mul = itertools.accumulate(numbers, operator.mul)
print("Акумуляція без функції")
print(list(accumulated_val))
print("Акумуляція з множенням")
print(list(accumulated_mul))

Вивід:

Акумуляція без функції
[1, 3, 6, 10, 15]
Акумуляція з множенням
[1, 2, 6, 24, 120]

#2. `chain()`

`chain(iterable_1, iterable_2, …)` приймає декілька ітераторів та об’єднує їх в один ітератор, що містить значення з усіх переданих ітераторів.

import itertools

letters = ['A', 'B', 'C', 'D']
numbers = [1, 2, 3]
colors = ['red', 'green', 'yellow']

# Об'єднання букв, чисел і кольорів
chained_iterable = list(itertools.chain(letters, numbers, colors))
print(chained_iterable)

Вивід:

['A', 'B', 'C', 'D', 1, 2, 3, 'red', 'green', 'yellow']

#3. `chain.from_iterable()`

`chain.from_iterable(iterable)` схожа на `chain()`, але вона приймає лише один ітератор, який містить підітератори, та об’єднує їх.

import itertools

letters = ['A', 'B', 'C', 'D']
numbers = [1, 2, 3]
colors = ['red', 'green', 'yellow']

iterable = ['hello',colors, letters, numbers]
chain = list(itertools.chain.from_iterable(iterable))
print(chain)

Вивід:

['h', 'e', 'l', 'l', 'o', 'red', 'green', 'yellow', 'A', 'B', 'C', 'D', 1, 2, 3]

#4. `compress()`

`compress(data, selectors)` приймає два аргументи: дані (`data`), що є ітератором, і селектори (`selectors`), які також є ітератором, що містить логічні значення `True` і `False`. Також можна використовувати `1` і `0` замість `True` і `False`. `compress()` фільтрує дані, вибираючи лише ті, що відповідають значенням `True` або `1` в `selectors`.

Значення в `data`, які відповідають `True` або `1` у `selectors`, вибираються, а решта ігноруються. Якщо в `selectors` менше логічних значень, ніж елементів в `data`, то елементи `data`, що виходять за межі `selectors`, ігноруються.

import itertools

# дані з 10 елементами
data = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J']
# 9 селекторів
selectors = [True, False, 1, False, 0, 1, True, False, 1]

# Вибір елементів на основі селекторів
filtered_data = list(itertools.compress(data, selectors))
print(filtered_data)

Вивід:

['A', 'C', 'F', 'G', 'I']

#5. `dropwhile()`

`dropwhile(function, sequence)` приймає функцію з умовою, яка повертає `True` або `False`, і послідовність значень. Функція видаляє всі значення з початку послідовності, поки умова повертає `True`. Як тільки умова стає `False`, всі інші елементи послідовності, незалежно від того, задовольняють вони умову чи ні, включаються до результату.

import itertools

numbers = [1, 2, 3, 4, 5, 1, 6, 7, 2, 1, 8, 9, 0, 7]

# Видаляємо елементи до тих пір, поки умова не буде False
filtered_numbers = list(itertools.dropwhile(lambda x: x < 5, numbers))
print(filtered_numbers)

Вивід:

[5, 1, 6, 7, 2, 1, 8, 9, 0, 7]

#6. `filterfalse()`

`filterfalse(function, sequence)` приймає функцію з умовою, що повертає `True` або `False`, та послідовність. Функція повертає значення з послідовності, які не задовольняють умову у функції.

import itertools

numbers = [1, 2, 3, 4, 2, 3, 5, 6, 5, 8, 1, 2, 3, 6, 2, 7, 4, 3]

# Фільтруємо елементи, для яких умова False
filtered_numbers = list(itertools.filterfalse(lambda x: x < 4, numbers))
print(filtered_numbers)

Вивід:

[4, 5, 6, 5, 8, 6, 7, 4]

#7. `groupby()`

`groupby(iterable, key)` приймає ітератор та ключ, а потім створює ітератор, який повертає послідовні ключі та групи. Для правильної роботи ітератор повинен бути відсортований за тією самою ключовою функцією. Ключова функція обчислює значення ключа для кожного елемента ітератора.

import itertools

input_list = [("Domestic", "Cow"), ("Domestic", "Dog"), ("Domestic", "Cat"),("Wild", "Lion"), ("Wild", "Zebra"), ("Wild", "Elephant")]
classification = itertools.groupby(input_list,lambda x: x[0])
for key,value in classification:
    print(key,":",list(value))

Вивід:

Domestic : [('Domestic', 'Cow'), ('Domestic', 'Dog'), ('Domestic', 'Cat')]
Wild : [('Wild', 'Lion'), ('Wild', 'Zebra'), ('Wild', 'Elephant')]

#8. `islice()`

`islice(iterable, start, stop, step)` дозволяє розділяти ітератор на основі значень `start`, `stop` і `step`. Аргумент `step` є необов’язковим. Нумерація починається з `0`, і елемент з індексом `stop` не включається до результату.

import itertools

numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]

# Вибір елементів у діапазоні
selected_numbers = list(itertools.islice(numbers, 2, 10))
selected_numbers_step= list(itertools.islice(numbers, 2, 10,2))
print("islice без кроку")
print(selected_numbers)
print("islice з кроком 2")
print(selected_numbers_step)

Вивід:

islice без кроку
[3, 4, 5, 6, 7, 8, 9, 10]
islice з кроком 2
[3, 5, 7, 9]

#9. `pairwise()`

`pairwise(iterable)` повертає послідовні пари елементів з ітератора в тому порядку, в якому вони з’являються. Якщо ітератор має менше двох елементів, результат буде порожнім.

from itertools import pairwise

numbers = [1, 2, 3, 4, 5, 6, 7, 8]
word = 'WORLD'
single = ['A']

print(list(pairwise(numbers)))
print(list(pairwise(word)))
print(list(pairwise(single)))

Вивід:

[(1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 7), (7, 8)]
[('W', 'O'), ('O', 'R'), ('R', 'L'), ('L', 'D')]
[]

#10. `starmap()`

`starmap(function, iterable)` – це функція, аналогічна `map()`, але вона використовується, коли параметри аргументів вже згруповані у кортежі. `starmap()` застосовує функцію до елементів ітератора, де кожен елемент ітератора – кортеж.

import itertools

iter_starmap = [(123, 63, 13), (5, 6, 52), (824, 51, 9), (26, 24, 16), (14, 15, 11)]
print (list(itertools.starmap(min, iter_starmap)))

Вивід:

[13, 5, 9, 16, 11]

#11. `takewhile()`

`takewhile(function, iterable)` працює навпаки до `dropwhile()`. Функція приймає умову для оцінки та ітератор. Вона включає всі елементи в ітератор, які задовольняють умову, доки умова не стане `False`. Коли умова стає `False`, усі наступні елементи ігноруються.

import itertools

numbers = [1, 2, 3, 4, 5, 1, 6, 7, 2, 1, 8, 9, 0, 7]

# Вибираємо елементи, доки умова не буде False
filtered_numbers = list(itertools.takewhile(lambda x: x < 5, numbers))
print(filtered_numbers)

Вивід:

[1, 2, 3, 4]

#12. `tee()`

`tee(iterable, n)` приймає ітератор і повертає `n` незалежних ітераторів. За замовчуванням `n` дорівнює 2.

import itertools

numbers = [1, 2, 3, 4, 5]

# Створення двох незалежних ітераторів
iter1, iter2 = itertools.tee(numbers, 2)
print(list(iter1))
print(list(iter2))

Вивід:

[1, 2, 3, 4, 5]
[1, 2, 3, 4, 5]

#13. `zip_longest()`

`zip_longest(iterables, fillvalue)` приймає декілька ітераторів та значення `fillvalue`. Функція повертає ітератор, що об’єднує елементи з переданих ітераторів. Якщо ітератори мають різну довжину, відсутні значення замінюються `fillvalue`, поки не буде вичерпано найдовший ітератор.

import itertools

names = ['John', 'mathew', 'mary', 'Alice', 'Bob', 'Charlie', 'Fury']
ages = [25, 30, 12, 13, 42]

# Об'єднання імен і віку, замінюючи відсутні значення "-"
combined = itertools.zip_longest(names, ages, fillvalue="-")

for name, age in combined:
    print(name, age)

Вивід:

John 25
mathew 30
mary 12
Alice 13
Bob 42
Charlie -
Fury -

Висновок

Модуль `itertools` є важливим інструментом для будь-якого розробника Python. Функції `itertools` широко використовуються в функціональному програмуванні, при обробці даних, їх фільтрації, групуванні, агрегації, об’єднанні ітерацій, комбінаториці та при роботі з нескінченними послідовностями.

Знання та вміння використовувати `itertools` суттєво покращить продуктивність та якість вашого коду. Не забудьте скористатися цією статтею як посібником у вивченні можливостей цього потужного модуля.