Domine o módulo Itertools Python: guia completo com exemplos

Explorando o Módulo Itertools em Python: Uma Abordagem Detalhada

Conforme a documentação oficial do Python, o módulo itertools é um recurso poderoso, que disponibiliza um conjunto de ferramentas otimizadas em termos de velocidade e uso de memória para manipulação de iteradores. Estas ferramentas, flexíveis e versáteis, podem ser usadas individualmente ou combinadas para criar e gerenciar iteradores de forma concisa e eficiente, principalmente quando lidamos com grandes volumes de dados.

O itertools oferece funcionalidades que simplificam o trabalho com iteradores, facilitando a construção de iteradores complexos a partir de iteradores mais simples. O uso deste módulo contribui para a redução de erros e para um código mais limpo, legível e de fácil manutenção.

As funcionalidades do módulo itertools podem ser categorizadas em três grupos principais, com base no comportamento dos iteradores que elas produzem:

Iteradores Infinitos

Estes iteradores são projetados para lidar com sequências potencialmente infinitas. Eles podem criar loops que se estenderão indefinidamente, a menos que uma condição de saída seja definida. São particularmente úteis para simular ciclos contínuos ou gerar sequências ilimitadas. O módulo itertools fornece três iteradores infinitos: count(), cycle() e repeat().

Iteradores Combinatórios

Este grupo engloba funções que facilitam a geração de produtos cartesianos e a execução de combinações e permutações sobre os elementos de um iterável. Estas ferramentas são fundamentais para explorar todas as possibilidades de organizar ou combinar elementos. O itertools disponibiliza quatro iteradores combinatórios: product(), permutations(), combinations() e combinations_with_replacement().

Iteradores de Terminação Baseados na Sequência de Entrada Mais Curta

Estes iteradores operam sobre sequências finitas e produzem uma saída específica, dependendo da função utilizada. Este grupo inclui: accumulate(), chain(), chain.from_iterable(), compress(), dropwhile(), filterfalse(), groupby(), islice(), pairwise(), starmap(), takewhile(), tee() e zip_longest().

Vamos agora explorar o funcionamento individual de cada função, agrupadas por tipo:

Iteradores Infinitos

Os três iteradores infinitos do módulo itertools são:

count()

A função count(start, step) gera uma progressão numérica infinita, iniciando com o valor start e incrementando-o a cada passo com o valor step. Ambos os argumentos são opcionais. Se start não for fornecido, o valor inicial será 0. Se step não for especificado, o incremento padrão será 1.

import itertools

# Contagem a partir de 4, com incremento de 2
for i in itertools.count(4, 2):
    # Condição para encerrar o loop, evitando iteração infinita
    if i == 14:
        break
    else:
        print(i) # Saída: 4, 6, 8, 10, 12

Saída:

4
6
8
10
12

cycle()

A função cycle(iterable) recebe um iterável e percorre seus elementos de forma cíclica. Após percorrer todos os itens, o ciclo reinicia, permitindo o acesso contínuo aos elementos na ordem original.

Ao utilizar cycle(), o resultado é armazenado em uma variável para criar um iterador que mantém seu estado. Isso garante que o ciclo não se reinicie a cada iteração, permitindo o acesso sequencial a cada elemento.

import itertools

cores = ["vermelho", "verde", "amarelo"]
# Passando cores para cycle()
ciclo_cores = itertools.cycle(cores)
print(ciclo_cores)

# Intervalo para interromper o loop infinito após 7 impressões
# next() para obter o próximo item do iterador
for i in range(7):
    print(next(ciclo_cores))

Saída:

vermelho
verde
amarelo
vermelho
verde
amarelo
vermelho

repeat()

A função repeat(elem, n) aceita um elemento (elem) e o repete n vezes. Se n não for especificado, o elemento será repetido infinitamente. O elemento pode ser um valor único ou um iterável.

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

Saída:

10 
10
10

Iteradores Combinatórios

Os iteradores combinatórios incluem:

product()

A função product() calcula o produto cartesiano dos iteráveis fornecidos. Dados dois iteráveis, por exemplo, x = {7, 8} e y = {1, 2, 3}, o produto cartesiano de x e y conterá todas as combinações possíveis de elementos de x e y, onde o primeiro elemento vem de x e o segundo vem de y. O produto cartesiano de x e y, neste caso, seria [(7, 1), (7, 2), (7, 3), (8, 1), (8, 2), (8, 3)].

product() aceita um parâmetro opcional chamado repeat, usado para calcular o produto cartesiano de um iterável consigo mesmo. O parâmetro repeat especifica quantas repetições serão consideradas para cada elemento durante o cálculo do produto cartesiano.

Por exemplo, product('ABCD', repeat=2) produzirá combinações como (‘A’, ‘A’), (‘A’, ‘B’), (‘A’, ‘C’), e assim por diante. Se repeat fosse definido como 3, a função geraria combinações como (‘A’, ‘A’, ‘A’), (‘A’, ‘A’, ‘B’), (‘A’, ‘A’, ‘C’), (‘A’, ‘A’, ‘D’) etc.

from itertools import product
# product() com o argumento opcional repeat
print("product() com o argumento opcional repeat")
print(list(product('ABC', repeat = 2)))

# product sem repeat
print("product() SEM um argumento opcional repeat")
print(list(product([7,8], [1,2,3])))

Saída:

product() com o argumento opcional repeat
[('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'B'), ('B', 'C'), ('C', 'A'), ('C', 'B'), ('C', 'C')]
product() SEM um argumento opcional repeat
[(7, 1), (7, 2), (7, 3), (8, 1), (8, 2), (8, 3)]

permutations()

permutations(iterable, group_size) retorna todas as permutações possíveis do iterável fornecido. Uma permutação representa o número de maneiras pelas quais os elementos de um conjunto podem ser ordenados. permutations() recebe um argumento opcional group_size. Se group_size não for especificado, as permutações geradas terão o mesmo tamanho que o comprimento do iterável passado para a função.

import itertools
numeros = [1, 2, 3]
permutacoes_com_tamanho = list(itertools.permutations(numeros,2))
permutacoes_sem_tamanho = list(itertools.permutations(numeros))

print("Permutações com tamanho 2")
print(permutacoes_com_tamanho)
print("Permutações SEM argumento de tamanho")
print(permutacoes_sem_tamanho)

Saída:

Permutações com tamanho 2
[(1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)]
Permutações SEM argumento de tamanho
[(1, 2, 3), (1, 3, 2), (2, 1, 3), (2, 3, 1), (3, 1, 2), (3, 2, 1)]

combinations()

combinations(iterable, size) retorna todas as combinações possíveis de um iterável, com um determinado comprimento, dos elementos no iterável. O argumento size especifica o tamanho de cada combinação.

Os resultados são ordenados. As combinações diferem ligeiramente das permutações. Em uma permutação, a ordem importa, mas em uma combinação a ordem não importa. Por exemplo, em [A, B, C] existem 6 permutações: AB, AC, BA, BC, CA, CB, mas apenas 3 combinações: AB, AC, BC.

import itertools
numeros = [1, 2, 3,4]
combinacao_tamanho2 = list(itertools.combinations(numeros,2))
combinacao_tamanho3 = list(itertools.combinations(numeros, 3))

print("Combinações com tamanho 2")
print(combinacao_tamanho2)
print("Combinações com tamanho 3")
print(combinacao_tamanho3)

Saída:

Combinações com tamanho 2
[(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
Combinações com tamanho 3
[(1, 2, 3), (1, 2, 4), (1, 3, 4), (2, 3, 4)]

combinations_with_replacement()

combinations_with_replacement(iterable, size) gera todas as combinações possíveis de um iterável de um determinado comprimento, permitindo a repetição de elementos nas combinações de saída. O argumento size define o tamanho das combinações geradas.

Esta função difere de combinations() porque fornece combinações em que um elemento pode ser repetido mais de uma vez. Por exemplo, é possível obter uma combinação como (1,1), o que não seria possível usando combinations().

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

combinacao_tamanho2 = list(itertools.combinations_with_replacement(numeros,2))
print("Combinations_with_replacement => tamanho 2")
print(combinacao_tamanho2)

Saída

Combinations_with_replacement => tamanho 2
[(1, 1), (1, 2), (1, 3), (1, 4), (2, 2), (2, 3), (2, 4), (3, 3), (3, 4), (4, 4)]

Iteradores de Terminação

Estes incluem iteradores como:

accumulate()

accumulate(iterable, function) aceita um iterável e uma função (opcional). Em seguida, retorna o resultado acumulado da aplicação da função a cada iteração nos elementos do iterável. Se nenhuma função for fornecida, a operação de soma é realizada e os resultados acumulados são retornados.

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

# Acumula a soma dos números
acumulado_soma = itertools.accumulate(numeros)
acumulado_multiplicacao = itertools.accumulate(numeros, operator.mul)
print("Accumulate sem função")
print(list(acumulado_soma))
print("Accumulate com multiplicação")
print(list(acumulado_multiplicacao))

Saída:

Accumulate sem função
[1, 3, 6, 10, 15]
Accumulate com multiplicação
[1, 2, 6, 24, 120]

chain()

chain(iterable_1, iterable_2, ...) recebe vários iteráveis e os encadeia, gerando um único iterável que contém os valores de todos os iteráveis passados para a função chain().

import itertools

letras = ['A', 'B', 'C', 'D']
numeros = [1, 2, 3]
cores = ['vermelho', 'verde', 'amarelo']

# Encadeando letras, números e cores juntos
encadeado_iteravel = list(itertools.chain(letras, numeros, cores))
print(encadeado_iteravel)

Saída:

['A', 'B', 'C', 'D', 1, 2, 3, 'vermelho', 'verde', 'amarelo']

chain.from_iterable()

chain.from_iterable(iterable) é semelhante a chain(). No entanto, difere, pois recebe apenas um único iterável que contém sub-iteráveis e os encadeia.

import itertools

letras = ['A', 'B', 'C', 'D']
numeros = [1, 2, 3]
cores = ['vermelho', 'verde', 'amarelo']

iteravel = ['ola', cores, letras, numeros]
corrente = list(itertools.chain.from_iterable(iteravel))
print(corrente)

Saída:

['o', 'l', 'a', 'vermelho', 'verde', 'amarelo', 'A', 'B', 'C', 'D', 1, 2, 3]

compress()

compress(data, selectors) aceita dois argumentos: data, que é um iterável, e selectors, que é um iterável que contém valores booleanos (True e False). 1 e 0 também podem ser usados como alternativas para os valores booleanos True e False. A função compress() filtra os elementos de data, usando os elementos correspondentes passados em selectors.

Os valores em data que correspondem a True ou 1 em selectors são selecionados, enquanto aqueles que correspondem a False ou 0 são ignorados. Se você passar menos booleanos em selectors do que o número de itens em data, todos os elementos além dos booleanos passados em selectors serão ignorados.

import itertools

# data tem 10 itens
data = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J']
# passando 9 seletores
seletores = [True, False, 1, False, 0, 1, True, False, 1]

# Selecionar elementos de data com base em seletores
filtrado_data = list(itertools.compress(data, seletores))
print(filtrado_data)

Saída:

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

dropwhile()

dropwhile(function, sequence) recebe uma função (com uma condição que retorna True ou False) e uma sequência de valores. Em seguida, descarta todos os valores até que a condição passada retorne False. Uma vez que a condição retorne False, o restante dos elementos serão incluídos nos resultados, independentemente de retornarem True ou False.

import itertools

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

# Descartar elementos até que a condição passada seja False
filtrado_numeros = list(itertools.dropwhile(lambda x: x < 5, numeros))
print(filtrado_numeros)

Saída:

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

filterfalse()

filterfalse(function, sequence) recebe uma função (com uma condição que é avaliada como True ou False) e uma sequência. Em seguida, retorna os valores da sequência que não satisfazem a condição da função.

import itertools

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

# Filtrar elementos para os quais a condição é False
filtrado_numeros = list(itertools.filterfalse(lambda x: x < 4, numeros))
print(filtrado_numeros)

Saída:

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

groupby()

groupby(iterable, key) recebe um iterável e uma chave, e então cria um iterador que retorna chaves e grupos consecutivos. Para que funcione, o iterável passado para ele precisa ser classificado pela mesma função de chave. A função de chave calcula um valor de chave para cada elemento no iterável.

import itertools

lista_entrada = [("Doméstico", "Vaca"), ("Doméstico", "Cachorro"), ("Doméstico", "Gato"), ("Selvagem", "Leão"), ("Selvagem", "Zebra"), ("Selvagem", "Elefante")]
classificacao = itertools.groupby(lista_entrada,lambda x: x[0])
for chave, valor in classificacao:
  print(chave, ":", list(valor))

Saída:

Doméstico : [('Doméstico', 'Vaca'), ('Doméstico', 'Cachorro'), ('Doméstico', 'Gato')]
Selvagem : [('Selvagem', 'Leão'), ('Selvagem', 'Zebra'), ('Selvagem', 'Elefante')]

islice()

islice(iterable, start, stop, step) permite fatiar um iterável usando os valores start, stop e step. O argumento step é opcional. A contagem começa em 0 e o item na posição de parada não é incluído.

import itertools

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

# Selecionar elementos dentro de um intervalo
selecionado_numeros = list(itertools.islice(numeros, 2, 10))
selecionado_numeros_step = list(itertools.islice(numeros, 2, 10,2))
print("islice sem definir um valor de step")
print(selecionado_numeros)
print("islice com um valor de step de 2")
print(selecionado_numeros_step)

Saída:

islice sem definir um valor de step
[3, 4, 5, 6, 7, 8, 9, 10]
islice com um valor de step de 2
[3, 5, 7, 9]

pairwise()

pairwise(iterable) retorna pares sobrepostos sucessivos retirados do iterável, na ordem em que aparecem no iterável. Se o iterável tiver menos de dois valores, o resultado de pairwise() estará vazio.

from itertools import pairwise

numeros = [1, 2, 3, 4, 5, 6, 7, 8]
palavra = 'MUNDO'
unico = ['A']

print(list(pairwise(numeros)))
print(list(pairwise(palavra)))
print(list(pairwise(unico)))

Saída:

[(1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 7), (7, 8)]
[('M', 'U'), ('U', 'N'), ('N', 'D'), ('D', 'O')]
[]

starmap()

starmap(function, iterable) é usada em vez de map() quando os argumentos da função já estão agrupados em tuplas. A função starmap() aplica uma função aos elementos do iterável passado. O iterável deve ter elementos agrupados em tuplas.

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)))

Saída:

[13, 5, 9, 16, 11]

takewhile()

takewhile(function, iterable) funciona de maneira oposta a dropwhile(). takewhile() recebe uma função com uma condição a ser avaliada e um iterável. Em seguida, inclui todos os elementos no iterável que satisfazem a condição até que False seja retornado. Depois que False for retornado, todos os elementos seguintes no iterável serão ignorados.

import itertools

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

# Incluir elementos até que a condição seja False
filtrado_numeros = list(itertools.takewhile(lambda x: x < 5, numeros))
print(filtrado_numeros)

Saída:

[1, 2, 3, 4]

tee()

tee(iterable, n) recebe um iterável e retorna vários iteradores independentes. O número de iteradores a serem retornados é definido por n, cujo valor padrão é 2.

import itertools

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

# Criar dois iteradores independentes a partir de numeros
iter1, iter2 = itertools.tee(numeros, 2)
print(list(iter1))
print(list(iter2))

Saída:

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

zip_longest()

zip_longest(iterables, fillvalue) aceita vários iteradores e um fillvalue. Em seguida, retorna um iterador que agrega elementos de cada um dos iteradores. Se os iteradores não tiverem o mesmo comprimento, os valores ausentes serão preenchidos com o fillvalue até que o iterável mais longo seja esgotado.

import itertools

nomes = ['João', 'Mateus', 'Maria', 'Alice', 'Bob', 'Charlie', 'Fúria']
idades = [25, 30, 12, 13, 42]

# Combinar nomes e idades, preenchendo as idades ausentes com um traço
combinado = itertools.zip_longest(nomes, idades, fillvalue="-")

for nome, idade in combinado:
    print(nome, idade)

Saída:

João 25
Mateus 30
Maria 12
Alice 13
Bob 42
Charlie -
Fúria -

Conclusão

O módulo itertools do Python oferece um conjunto de ferramentas muito importante para desenvolvedores. O itertools é amplamente utilizado em programação funcional, no processamento e transformação de dados, na filtragem e seleção de dados, no agrupamento e agregação, na combinação de iteráveis, em combinatória e ao trabalhar com sequências infinitas.

Como desenvolvedor Python, você se beneficiará muito ao aprender sobre o itertools, portanto, utilize este artigo para se familiarizar com o módulo.