O módulo itertools é uma das joias da biblioteca padrão do Python. Ele fornece um conjunto de funções que criam iteradores eficientes para loops, combinando conceitos de programação funcional e manipulação de dados. Essas funções são projetadas para serem rápidas e econômicas em memória, tornando o código mais limpo e expressivo. Nesta aula, vamos explorar algumas das funções mais úteis do módulo, com exemplos práticos e casos de uso do mundo real.

Entender o itertools é essencial para quem deseja escrever código Python idiomático e eficiente. Em vez de reinventar a roda com loops aninhados ou listas intermediárias, você pode usar essas ferramentas compostas para criar pipelines de dados sofisticados. Vamos mergulhar nas funções principais e ver como elas podem simplificar seu dia a dia como programador.

count, cycle, chain

A função count() cria um iterador que gera números consecutivos, começando de um valor inicial e incrementando por um passo especificado. É útil para gerar índices ou contadores infinitos, mas lembre-se de que, como é infinito, você deve usá-lo com cuidado, geralmente com islice() ou em loops com condição de saída.

cycle() itera sobre uma sequência indefinidamente, repetindo-a após chegar ao fim. É perfeito para alternar entre valores ou criar padrões repetitivos. Por fim, chain() combina vários iteráveis em um único iterador, percorrendo-os sequencialmente. Isso é ideal para concatenar listas, tuplas ou qualquer outro iterável sem criar cópias desnecessárias.

from itertools import count, cycle, chain, islice

# count: gera números infinitos
contador = count(start=10, step=2)
primeiros = list(islice(contador, 5))
print(primeiros)  # [10, 12, 14, 16, 18]

# cycle: repete uma sequência infinitamente
ciclo = cycle(['vermelho', 'verde', 'azul'])
print(list(islice(ciclo, 5)))  # ['vermelho', 'verde', 'azul', 'vermelho', 'verde']

# chain: concatena iteráveis
lista1 = [1, 2, 3]
lista2 = [4, 5]
concatenado = list(chain(lista1, lista2))
print(concatenado)  # [1, 2, 3, 4, 5]

Essas funções são blocos de construção para iteradores mais complexos. Elas evitam a criação de listas intermediárias, economizando memória, especialmente quando se trabalha com grandes volumes de dados. Por exemplo, em vez de lista1 + lista2 (que cria uma nova lista), chain apenas percorre os elementos sem cópia.

combinations e permutations

As funções combinations() e permutations() geram todas as combinações e permutações possíveis de um iterável, respectivamente. Uma combinação ignora a ordem, enquanto uma permutação considera a ordem. Ambas são extremamente úteis em problemas de matemática, jogos, análise de dados e otimização.

Por exemplo, se você precisa testar todas as formas de escolher 2 itens de uma lista, combinations retorna os pares únicos. Já permutations retorna todos os arranjos ordenados. É importante notar que o número de resultados cresce rapidamente, então use com moderação.

from itertools import combinations, permutations

letras = ['A', 'B', 'C']

# Combinações de 2 elementos (ordem não importa)
combos = list(combinations(letras, 2))
print(combos)  # [('A', 'B'), ('A', 'C'), ('B', 'C')]

# Permutações de 2 elementos (ordem importa)
perms = list(permutations(letras, 2))
print(perms)  # [('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')]

Essas funções são implementadas de forma eficiente, não gerando todas as combinações de uma vez, mas sim uma por vez, o que permite iterar sobre elas sem estourar a memória. Isso é crucial em problemas de força bruta ou quando se explora um espaço de busca grande.

groupby

A função groupby() agrupa elementos consecutivos de um iterável que compartilham o mesmo valor de uma chave. É importante destacar que ela agrupa apenas elementos consecutivos, então a sequência deve estar ordenada pela chave para agrupar todos os elementos semelhantes. Caso contrário, você pode obter múltiplos grupos para a mesma chave.

O retorno é um iterador de tuplas (chave, grupo), onde grupo é um iterador dos elementos que pertencem àquele grupo. É uma ferramenta poderosa para processamento de dados, como agrupar registros por categoria, desde que os dados estejam ordenados.

from itertools import groupby

dados = [
    {'nome': 'Ana', 'cidade': 'SP'},
    {'nome': 'Bruno', 'cidade': 'RJ'},
    {'nome': 'Carla', 'cidade': 'SP'},
    {'nome': 'Diego', 'cidade': 'RJ'},
]

# Ordenar por cidade para agrupar corretamente
dados_ordenados = sorted(dados, key=lambda x: x['cidade'])

for cidade, grupo in groupby(dados_ordenados, key=lambda x: x['cidade']):
    nomes = [pessoa['nome'] for pessoa in grupo]
    print(f'{cidade}: {nomes}')
# Saída:
# RJ: ['Bruno', 'Diego']
# SP: ['Ana', 'Carla']

Neste exemplo, os dados foram ordenados por cidade antes de usar groupby. Sem essa ordenação, teríamos grupos separados para cada ocorrência de 'SP', o que não é desejado. Portanto, lembre-se sempre de ordenar quando necessário.

Casos de uso

O itertools é amplamente utilizado em várias áreas. Em análise de dados, groupby é usado para agregar informações por categoria. Em simulações, cycle e count ajudam a criar loops infinitos controlados. Em algoritmos de busca, combinations e permutations exploram todas as possibilidades. Além disso, funções como islice, takewhile e dropwhile (não abordadas aqui) complementam o kit.

Vamos ver um exemplo prático: gerar uma lista de horários em intervalos de 30 minutos usando count e islice. Ou ainda, criar um sistema de rodízio de tarefas com cycle. Esses casos mostram como o módulo resolve problemas de forma concisa e legível.

from itertools import count, islice, cycle

# Gerar os primeiros 10 horários a partir de 9:00 com incremento de 30 minutos
inicio = 9 * 60  # 9:00 em minutos
horarios = [f'{h//60:02d}:{h%60:02d}' for h in islice(count(inicio, 30), 10)]
print(horarios)
# ['09:00', '09:30', '10:00', '10:30', '11:00', '11:30', '12:00', '12:30', '13:00', '13:30']

# Rodízio de pessoas para limpeza
pessoas = ['Alice', 'Bob', 'Carol']
escalas = list(islice(cycle(pessoas), 7))
print(escalas)  # ['Alice', 'Bob', 'Carol', 'Alice', 'Bob', 'Carol', 'Alice']

Esses exemplos ilustram como você pode usar itertools para criar soluções elegantes para problemas comuns. Dominar essas funções fará seu código mais Pythonico e eficiente.

Boas práticas e observações finais

Ao usar itertools, lembre-se de que muitas funções retornam iteradores, não listas. Isso significa que elas são preguiçosas e só geram elementos quando consumidos. Isso é ótimo para eficiência, mas pode causar surpresas se você tentar reutilizar o iterador. Sempre converta para lista ou use list() se precisar de uma coleção reutilizável.

Outra dica: combine itertools com funções como map, filter e functools.reduce para criar pipelines de processamento de dados. Isso promove um estilo de programação funcional que é poderoso e expressivo. Finalmente, consulte a documentação oficial para explorar mais funções como product, zip_longest e accumulate.

Referências

Exercícios

  1. Exercício 1: Use count() e islice() para gerar os primeiros 15 números pares (começando em 0). Imprima a lista.
  2. ✓ Resposta:
    from itertools import count, islice
    
    pares = list(islice(count(0, 2), 15))
    print(pares)  # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28]
  3. Exercício 2: Dada a string "ABC", use combinations() para listar todas as combinações de 2 letras. Imprima o resultado.
  4. ✓ Resposta:
    from itertools import combinations
    
    letras = "ABC"
    combos = list(combinations(letras, 2))
    print(combos)  # [('A', 'B'), ('A', 'C'), ('B', 'C')]
  5. Exercício 3: Crie um iterador com cycle() que repita a sequência [1, 2, 3] e extraia os primeiros 10 elementos usando islice(). Imprima a lista.
  6. ✓ Resposta:
    from itertools import cycle, islice
    
    ciclo = cycle([1, 2, 3])
    primeiros = list(islice(ciclo, 10))
    print(primeiros)  # [1, 2, 3, 1, 2, 3, 1, 2, 3, 1]
  7. Exercício 4: Use groupby() para agrupar uma lista de números por paridade (par/ímpar). Lembre-se de ordenar antes. A lista é [1, 2, 3, 4, 5]. Imprima cada grupo.
  8. ✓ Resposta:
    from itertools import groupby
    
    nums = [1, 2, 3, 4, 5]
    ordenados = sorted(nums, key=lambda x: x % 2)
    for chave, grupo in groupby(ordenados, key=lambda x: x % 2):
        print(f"{'par' if chave == 0 else 'ímpar'}: {list(grupo)}")
    # Saída:
    # ímpar: [1, 3, 5]
    # par: [2, 4]
  9. Exercício 5: Escreva uma função que receba uma lista de nomes e use chain() para concatenar duas listas de nomes (primeiros e últimos) em uma única lista, preservando a ordem. Teste com as listas ["Ana", "Bruno"] e ["Silva", "Souza"].
  10. ✓ Resposta:
    from itertools import chain
    
    def concatenar_listas(lista1, lista2):
        return list(chain(lista1, lista2))
    
    primeiros = ["Ana", "Bruno"]
    sobrenomes = ["Silva", "Souza"]
    print(concatenar_listas(primeiros, sobrenomes))  # ['Ana', 'Bruno', 'Silva', 'Souza']