O módulo itertools é um dos tesouros escondidos da biblioteca padrão do Python. Ele fornece um conjunto de funções que criam iteradores para processamento eficiente de dados, seguindo princípios de programação funcional. Essas funções são projetadas para serem rápidas, econômicas em memória e compostas entre si, permitindo que você construa pipelines de dados complexos com pouco código.

Nesta aula, vamos mergulhar nas funções mais importantes do itertools: count, cycle, chain, combinations, permutations e groupby. Cada uma resolve um problema comum de iteração de forma elegante. Ao final, você verá como essas ferramentas se encaixam em casos de uso do mundo real, desde geração de números infinitos até agrupamento de dados.

count, cycle, chain

Começamos com três funções que lidam com a criação e combinação de iteradores.

itertools.count(start=0, step=1) gera uma sequência infinita de números. É útil para criar contadores, índices ou para simular loops com base em condições. Como é infinito, você deve usá-lo com islice ou takewhile para limitar a iteração.

itertools.cycle(iterable) repete um iterável indefinidamente. Por exemplo, cycle('AB') produz A, B, A, B, ... para sempre. É útil para alternar entre valores, como em turnos de trabalho ou cores em um gráfico.

itertools.chain(*iterables) combina vários iteráveis em uma única sequência, como se fossem concatenados. É uma alternativa mais eficiente a + para listas, pois não cria uma nova lista, apenas um iterador.

import itertools

# count: números infinitos a partir de 10, passo 2
contador = itertools.count(10, 2)
print(list(itertools.islice(contador, 5)))  # [10, 12, 14, 16, 18]

# cycle: repete uma lista infinitamente
ciclo = itertools.cycle(['vermelho', 'verde', 'azul'])
print(list(itertools.islice(ciclo, 5)))  # ['vermelho', 'verde', 'azul', 'vermelho', 'verde']

# chain: concatena várias listas
lista1 = [1, 2, 3]
lista2 = [4, 5]
lista3 = [6]
combinado = itertools.chain(lista1, lista2, lista3)
print(list(combinado))  # [1, 2, 3, 4, 5, 6]

Essas funções são a base para construir iteradores personalizados. Elas evitam o desperdício de memória e tornam o código mais expressivo.

combinations e permutations

Quando você precisa gerar combinações ou permutações de um conjunto de elementos, o itertools oferece funções prontas e eficientes.

itertools.combinations(iterable, r) gera todas as combinações de comprimento r dos elementos do iterável, sem repetição e sem ordem. Por exemplo, as combinações de [1,2,3] com r=2 são (1,2), (1,3), (2,3).

itertools.permutations(iterable, r=None) gera todas as permutações de comprimento r (se r não for informado, usa o comprimento do iterável), ou seja, todas as ordenações possíveis. Por exemplo, as permutações de [1,2,3] com r=2 são (1,2), (1,3), (2,1), (2,3), (3,1), (3,2).

Essas funções são extremamente úteis em problemas de otimização, jogos, criptografia e qualquer área que exija explorar arranjos de elementos.

import itertools

# Combinações de 2 elementos da lista
letras = ['A', 'B', 'C']
comb = itertools.combinations(letras, 2)
print(list(comb))  # [('A', 'B'), ('A', 'C'), ('B', 'C')]

# Permutações de 2 elementos
perm = itertools.permutations(letras, 2)
print(list(perm))  # [('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')]

Note que as combinações ignoram a ordem, enquanto as permutações a consideram. Isso é crucial na escolha da função adequada.

groupby

A função itertools.groupby(iterable, key=None) agrupa elementos consecutivos que têm o mesmo valor de chave. Ela retorna um iterador de pares (chave, grupo), onde grupo é um iterador para os elementos consecutivos com aquela chave.

É importante ressaltar que groupby só agrupa itens consecutivos. Se os dados não estiverem ordenados pela chave, os grupos serão fragmentados. Portanto, é comum ordenar o iterável antes de aplicar groupby.

O parâmetro key é uma função que extrai a chave de cada elemento. Se não for fornecido, o próprio elemento é usado como chave.

import itertools

dados = [('A', 1), ('A', 2), ('B', 3), ('B', 4), ('A', 5)]
# Ordenar por letra para agrupar corretamente
dados_ordenados = sorted(dados, key=lambda x: x[0])

for letra, grupo in itertools.groupby(dados_ordenados, key=lambda x: x[0]):
    print(letra, list(grupo))
# A [('A', 1), ('A', 2)]
# B [('B', 3), ('B', 4)]
# A [('A', 5)]  (novo grupo porque não é consecutivo após ordenação? Na verdade, após ordenar, todos os A vêm primeiro, então o último A é consecutivo, mas o exemplo mostra que sem ordenar, os grupos são fragmentados)

No exemplo acima, se não ordenássemos, os grupos seriam: A, B, A. Com a ordenação, todos os A ficam juntos, formando um único grupo. Isso mostra a importância de pré-processar os dados.

Casos de uso

O itertools é amplamente usado em análise de dados, processamento de logs, geração de sequências e até em algoritmos de aprendizado de máquina. Vamos ver alguns exemplos práticos:

  • Gerar números de página infinitos: Com count, você pode criar um gerador de números de página que nunca acaba, útil para paginação de APIs.
  • Alternar entre estados: Com cycle, você pode alternar entre modos (ex.: ligado/desligado) em um loop.
  • Combinar múltiplas fontes de dados: chain é perfeito para processar vários arquivos ou listas como se fossem um único fluxo.
  • Gerar pares de teste: combinations e permutations são usados em testes para gerar todas as entradas possíveis.
  • Agrupar logs por data: groupby pode agrupar entradas de log por dia, hora, etc., desde que os dados estejam ordenados.
import itertools

# Exemplo: gerar combinações de times para um torneio
times = ['Time A', 'Time B', 'Time C', 'Time D']
partidas = itertools.combinations(times, 2)
print('Partidas:')
for partida in partidas:
    print(f'{partida[0]} vs {partida[1]}')

# Exemplo: agrupar números por paridade (par/ímpar)
numeros = [1, 2, 3, 4, 5, 6]
numeros_ord = sorted(numeros, key=lambda x: x % 2)
for chave, grupo in itertools.groupby(numeros_ord, key=lambda x: x % 2):
    print('Par' if chave == 0 else 'Ímpar', list(grupo))

Esses exemplos mostram como o itertools pode simplificar tarefas que, de outra forma, exigiriam loops complexos e gerenciamento manual de índices.

Boas práticas e observações finais

Ao usar itertools, lembre-se de que os iteradores são lazy: eles calculam os valores sob demanda. Isso é ótimo para economia de memória, mas você deve ter cuidado ao convertê-los em listas se o conjunto for grande.

Combine funções do itertools com outras funções como map, filter e zip para criar pipelines poderosos. Por exemplo, você pode usar itertools.islice para limitar um count ou takewhile para parar quando uma condição for atingida.

Além disso, consulte a documentação oficial para explorar outras funções úteis como product, tee, zip_longest, etc. Elas podem resolver problemas que você nem imaginava que tinham solução pronta.

Referências

Exercícios

  1. Exercício 1: Crie um gerador que produza os primeiros 10 números da sequência de Fibonacci usando itertools.islice e uma função geradora.

    ✓ Resposta:
    import itertools
    
    def fibonacci():
        a, b = 0, 1
        while True:
            yield a
            a, b = b, a + b
    
    print(list(itertools.islice(fibonacci(), 10)))
    # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
  2. Exercício 2: Use itertools.cycle para criar um loop que alterne entre as strings "par" e "ímpar" para os números de 0 a 9, e imprima cada número com sua classificação.

    ✓ Resposta:
    import itertools
    
    rotulo = itertools.cycle(['par', 'ímpar'])
    for i in range(10):
        print(f'{i} é {next(rotulo)}')
  3. Exercício 3: Dada uma lista de palavras, use itertools.chain para combinar as letras de todas as palavras em uma única lista de caracteres. Por exemplo, ['ab', 'cde'] deve resultar em ['a', 'b', 'c', 'd', 'e'].

    ✓ Resposta:
    import itertools
    
    palavras = ['ab', 'cde']
    letras = list(itertools.chain.from_iterable(palavras))
    print(letras)  # ['a', 'b', 'c', 'd', 'e']
  4. Exercício 4: Gere todas as permutações possíveis da string "ABC" e imprima-as em ordem lexicográfica. Use itertools.permutations e ordene o resultado.

    ✓ Resposta:
    import itertools
    
    permutacoes = itertools.permutations('ABC')
    perm_ordenadas = sorted(permutacoes)
    for perm in perm_ordenadas:
        print(''.join(perm))
    # ABC
    # ACB
    # BAC
    # BCA
    # CAB
    # CBA
  5. Exercício 5: Use itertools.groupby para agrupar os números de 1 a 10 por sua divisibilidade por 3 (resto 0, 1 ou 2). Imprima cada grupo com sua chave.

    ✓ Resposta:
    import itertools
    
    numeros = range(1, 11)
    numeros_ord = sorted(numeros, key=lambda x: x % 3)
    for chave, grupo in itertools.groupby(numeros_ord, key=lambda x: x % 3):
        print(f'Resto {chave}: {list(grupo)}')
    # Resto 0: [3, 6, 9]
    # Resto 1: [1, 4, 7, 10]
    # Resto 2: [2, 5, 8]