Python Admin

O que Todo Dev Deve Saber sobre Programação Funcional em Python: map, filter, functools e itertools Já leu

Fundamentos da Programação Funcional em Python A programação funcional é um paradigma que trata a computação como a avaliação de funções matemáticas, evitando mudança de estado e dados mutáveis. Python, embora seja multiparadigma, oferece suporte robusto para este estilo através de construções nativas. A grande vantagem dessa abordagem é escrever código mais previsível, testável e fácil de raciocinar, já que funções puras (aquelas que não modificam estado externo e retornam sempre o mesmo resultado para as mesmas entradas) são o coração dessa filosofia. No contexto prático, programação funcional em Python significa trabalhar com operações sobre coleções de dados de forma declarativa — você descreve o quê quer fazer, não como fazer. Isso contrasta com a programação imperativa, onde você detalha cada passo da execução. As ferramentas que veremos ( , , e ) são exatamente os instrumentos que facilitam essa transformação mental e prática do seu código. A Função : Transformando Dados O Conceito por Trás do A função aplica

Fundamentos da Programação Funcional em Python

A programação funcional é um paradigma que trata a computação como a avaliação de funções matemáticas, evitando mudança de estado e dados mutáveis. Python, embora seja multiparadigma, oferece suporte robusto para este estilo através de construções nativas. A grande vantagem dessa abordagem é escrever código mais previsível, testável e fácil de raciocinar, já que funções puras (aquelas que não modificam estado externo e retornam sempre o mesmo resultado para as mesmas entradas) são o coração dessa filosofia.

No contexto prático, programação funcional em Python significa trabalhar com operações sobre coleções de dados de forma declarativa — você descreve o quê quer fazer, não como fazer. Isso contrasta com a programação imperativa, onde você detalha cada passo da execução. As ferramentas que veremos (map, filter, functools e itertools) são exatamente os instrumentos que facilitam essa transformação mental e prática do seu código.

A Função map(): Transformando Dados

O Conceito por Trás do map()

A função map() aplica uma função a cada elemento de uma sequência e retorna um iterador com os resultados. Ela é fundamental quando você precisa transformar todos os elementos de uma coleção de forma uniforme. Em vez de escrever um loop for, você expressa a intenção de forma mais limpa e direta.

# Abordagem imperativa (tradicional)
números = [1, 2, 3, 4, 5]
quadrados = []
for num in números:
    quadrados.append(num ** 2)
print(quadrados)  # [1, 4, 9, 16, 25]

# Abordagem funcional com map()
números = [1, 2, 3, 4, 5]
quadrados = list(map(lambda x: x ** 2, números))
print(quadrados)  # [1, 4, 9, 16, 25]

Note que map() retorna um iterador, não uma lista. Isso é uma otimização de memória — em Python 3, map() é "lazy" (preguiçoso), só computando valores conforme necessário. Se você precisar de uma lista imediata, use list().

Usando Funções Nomeadas com map()

Embora lambdas sejam convenientes, funções nomeadas tornam o código mais legível quando a lógica é complexa. Veja como map() trabalha perfeitamente com qualquer função:

def converter_celsius_para_fahrenheit(celsius):
    """Converte temperatura de Celsius para Fahrenheit."""
    return (celsius * 9/5) + 32

temperaturas_celsius = [0, 10, 20, 30, 40]
temperaturas_fahrenheit = list(map(converter_celsius_para_fahrenheit, temperaturas_celsius))
print(temperaturas_fahrenheit)  # [32.0, 50.0, 68.0, 86.0, 104.0]

map() com Múltiplas Sequências

Uma capacidade poderosa do map() é processar múltiplas sequências em paralelo, passando elementos correspondentes para a função:

# Combinando duas listas
nomes = ["Alice", "Bob", "Carlos"]
idades = [25, 30, 28]

# Função que recebe dois argumentos
def apresentar(nome, idade):
    return f"{nome} tem {idade} anos"

apresentacoes = list(map(apresentar, nomes, idades))
for apresentacao in apresentacoes:
    print(apresentacao)
# Alice tem 25 anos
# Bob tem 30 anos
# Carlos tem 28 anos

A Função filter(): Selecionando Dados Relevantes

O Conceito e Aplicação Prática

filter() reduz uma sequência mantendo apenas os elementos que atendem a um critério. A função passada como argumento deve retornar um valor booleano. Como map(), filter() também retorna um iterador lazy, economizando memória em grandes datasets.

# Abordagem imperativa
números = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
pares = []
for num in números:
    if num % 2 == 0:
        pares.append(num)
print(pares)  # [2, 4, 6, 8, 10]

# Abordagem funcional com filter()
números = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
pares = list(filter(lambda x: x % 2 == 0, números))
print(pares)  # [2, 4, 6, 8, 10]

Combinando map() e filter()

A verdadeira potência da programação funcional emerge quando você combina operações. Um padrão comum é filtrar dados e depois transformá-los:

# Dados brutos de vendas
vendas = [
    {"produto": "Notebook", "valor": 3500, "vendido": True},
    {"produto": "Mouse", "valor": 50, "vendido": False},
    {"produto": "Teclado", "valor": 200, "vendido": True},
    {"produto": "Monitor", "valor": 1200, "vendido": True},
]

# Passo 1: Filtrar apenas vendas realizadas
# Passo 2: Extrair apenas o valor de cada venda
vendas_realizadas = list(
    map(
        lambda v: v["valor"],
        filter(lambda v: v["vendido"], vendas)
    )
)
print(vendas_realizadas)  # [3500, 200, 1200]
print(f"Total: R$ {sum(vendas_realizadas)}")  # Total: R$ 4900

Filtrando com Funções Nomeadas

Para lógica mais complexa, usar funções nomeadas melhora a legibilidade:

def é_ativo(usuario):
    """Verifica se um usuário está ativo."""
    return usuario.get("ativo", False)

usuários = [
    {"nome": "Ana", "ativo": True},
    {"nome": "Bruno", "ativo": False},
    {"nome": "Carlos", "ativo": True},
]

usuários_ativos = list(filter(é_ativo, usuários))
print(usuários_ativos)
# [{'nome': 'Ana', 'ativo': True}, {'nome': 'Carlos', 'ativo': True}]

O Módulo functools: Operações Avançadas sobre Funções

reduce(): Agregando Dados

functools.reduce() combina todos os elementos de uma sequência usando uma função binária (que recebe dois argumentos), retornando um único valor. É essencial para operações cumulativas como soma, produto ou concatenação:

from functools import reduce

# Somando números
números = [1, 2, 3, 4, 5]
soma = reduce(lambda x, y: x + y, números)
print(soma)  # 15

# Encontrando o maior valor
números = [3, 7, 2, 9, 1]
máximo = reduce(lambda x, y: x if x > y else y, números)
print(máximo)  # 9

# Multiplicando todos os elementos (fatorial)
números = [1, 2, 3, 4, 5]
fatorial = reduce(lambda x, y: x * y, números)
print(fatorial)  # 120

Usando reduce() com Funções Nomeadas

Para operações complexas, uma função nomeada deixa a intenção clara:

from functools import reduce

def combinar_dicts(dict1, dict2):
    """Combina dois dicionários, mantendo valores do primeiro em caso de conflito."""
    resultado = {**dict1, **dict2}
    return resultado

dicts = [
    {"a": 1, "b": 2},
    {"b": 3, "c": 4},
    {"c": 5, "d": 6},
]

resultado_final = reduce(combinar_dicts, dicts)
print(resultado_final)  # {'a': 1, 'b': 3, 'c': 5, 'd': 6}

partial(): Criando Variações de Funções

functools.partial() cria uma nova função fixando alguns argumentos de uma função existente. É útil para adaptar funções genéricas a contextos específicos:

from functools import partial

def potência(base, expoente):
    return base ** expoente

# Criando uma função especializada
ao_quadrado = partial(potência, expoente=2)
ao_cubo = partial(potência, expoente=3)

print(ao_quadrado(5))  # 25
print(ao_cubo(5))      # 125

# Exemplo prático: adaptando uma função de processamento de strings
def formatar_valor(formato, valor):
    """Formata um valor segundo um padrão."""
    return formato.format(valor)

formatar_moeda = partial(formatar_valor, formato="R$ {:.2f}")
formatar_porcentagem = partial(formatar_valor, formato="{:.1%}")

print(formatar_moeda(1234.5678))      # R$ 1234.57
print(formatar_porcentagem(0.856))    # 85.6%

cache(): Otimizando Chamadas Repetidas

O decorador @functools.cache armazena resultados de chamadas anteriores, evitando recálculos desnecessários. É perfeito para funções puras e custosas:

from functools import cache
import time

@cache
def fibonacci(n):
    """Calcula o n-ésimo número de Fibonacci com cache."""
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

# Primeira chamada (recalcula)
início = time.time()
resultado1 = fibonacci(35)
tempo1 = time.time() - início

# Segunda chamada (usa cache)
início = time.time()
resultado2 = fibonacci(35)
tempo2 = time.time() - início

print(f"Resultado: {resultado1}")
print(f"Primeira chamada: {tempo1:.4f}s")
print(f"Segunda chamada: {tempo2:.6f}s (muito mais rápida!)")

O Módulo itertools: Combinações e Permutações

chain(): Encadeando Sequências

itertools.chain() concatena múltiplas sequências em um único iterador, sem criar cópias intermediárias:

from itertools import chain

lista1 = [1, 2, 3]
lista2 = [4, 5, 6]
lista3 = [7, 8, 9]

# Encadeando três listas
todas = chain(lista1, lista2, lista3)
print(list(todas))  # [1, 2, 3, 4, 5, 6, 7, 8, 9]

# Muito mais eficiente que concatenação com +
# Especialmente importante em loops com muitas sequências
for valor in chain([1, 2], [3, 4], [5, 6]):
    print(valor)

repeat() e cycle(): Iteradores Infinitos

Esses geradores criam iteradores "infinitos" que são úteis em combinação com funções como zip() e map():

from itertools import repeat, cycle, islice

# repeat() gera o mesmo valor N vezes (ou infinitamente)
print(list(repeat("A", 3)))  # ['A', 'A', 'A']

# cycle() percorre uma sequência continuamente
contador = cycle([1, 2, 3])
print(list(islice(contador, 7)))  # [1, 2, 3, 1, 2, 3, 1]

# Caso prático: aplicar a mesma operação com valores diferentes
preços_unitários = [10, 20, 15]
quantidades = [5, 3, 4]

total_por_produto = list(map(lambda p, q: p * q, preços_unitários, quantidades))
print(total_por_produto)  # [50, 60, 60]

combinations() e permutations(): Gerando Variações

Essas funções geram todas as combinações ou permutações possíveis de uma sequência:

from itertools import combinations, permutations

# Combinações: subconjuntos sem considerar ordem
sabores = ["Chocolate", "Baunilha", "Morango"]
duplas = combinations(sabores, 2)
print(list(duplas))
# [('Chocolate', 'Baunilha'), ('Chocolate', 'Morango'), ('Baunilha', 'Morango')]

# Permutações: ordenações diferentes
dois_algarismos = permutations([1, 2, 3], 2)
print(list(dois_algarismos))
# [(1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)]

# Caso prático: gerar todas as combinações possíveis de testes
navegadores = ["Chrome", "Firefox"]
sistemas_operacionais = ["Windows", "Linux"]
combinações_testes = list(combinations(navegadores + sistemas_operacionais, 2))
# Você agora tem todas as duplas para teste de compatibilidade

islice(): Extração Elegante de Subsequências

itertools.islice() extrai uma fatia de um iterador sem convertê-lo em lista, mantendo a eficiência de memória:

from itertools import islice, count

# Pegando os primeiros 5 números naturais
primeiros_cinco = islice(count(1), 5)
print(list(primeiros_cinco))  # [1, 2, 3, 4, 5]

# Pulando elementos: começar no índice 2, pegar 4 elementos
números = range(10)
seleção = islice(números, 2, 6)
print(list(seleção))  # [2, 3, 4, 5]

# Processando apenas os primeiros 1000 registros de um grande arquivo
def ler_logs_do_servidor():
    """Simula leitura infinita de logs."""
    contador = 0
    while True:
        contador += 1
        yield f"Log {contador}: evento processado"

# Pegar apenas os 10 primeiros logs
primeiros_logs = list(islice(ler_logs_do_servidor(), 10))
for log in primeiros_logs:
    print(log)

groupby(): Agrupando Dados por Chave

itertools.groupby() agrupa elementos consecutivos que compartilham a mesma chave:

from itertools import groupby
from operator import itemgetter

# Agrupando números por paridade
números = [1, 3, 5, 2, 4, 6, 7, 9]
agrupados = groupby(sorted(números, key=lambda x: x % 2), key=lambda x: x % 2)

for chave, grupo in agrupados:
    tipo = "par" if chave == 0 else "ímpar"
    print(f"{tipo}: {list(grupo)}")
# par: [2, 4, 6]
# ímpar: [1, 3, 5, 7, 9]

# Agrupando registros por categoria
vendas = [
    {"produto": "Notebook", "categoria": "Eletrônicos", "vendas": 10},
    {"produto": "Mouse", "categoria": "Eletrônicos", "vendas": 25},
    {"produto": "Livro", "categoria": "Livros", "vendas": 15},
    {"produto": "Caneta", "categoria": "Papelaria", "vendas": 50},
]

# Primeiro, ordenar pela categoria
vendas_ordenadas = sorted(vendas, key=itemgetter("categoria"))

# Depois, agrupar
for categoria, grupo in groupby(vendas_ordenadas, key=itemgetter("categoria")):
    produtos = [item["produto"] for item in grupo]
    print(f"{categoria}: {produtos}")
# Eletrônicos: ['Notebook', 'Mouse']
# Livros: ['Livro']
# Papelaria: ['Caneta']

Um Exemplo Integrado: Pipeline Funcional Completo

Para solidificar o aprendizado, vamos construir um pipeline real que combine todas essas ferramentas:

from functools import reduce
from itertools import filterfalse
import operator

# Dataset: pedidos de um e-commerce
pedidos = [
    {"id": 1, "cliente": "Ana", "itens": [{"produto": "Notebook", "preço": 3000, "qtd": 1}]},
    {"id": 2, "cliente": "Bruno", "itens": [{"produto": "Mouse", "preço": 50, "qtd": 2}]},
    {"id": 3, "cliente": "Carlos", "itens": [{"produto": "Teclado", "preço": 200, "qtd": 1}, {"produto": "Monitor", "preço": 1200, "qtd": 1}]},
    {"id": 4, "cliente": "Ana", "itens": []},  # Pedido vazio
]

# Pipeline:
# 1. Filtrar pedidos que têm itens
# 2. Para cada pedido, calcular o valor total
# 3. Ordenar por valor descendente
# 4. Exibir resultado

def calcular_valor_pedido(pedido):
    """Calcula o valor total de um pedido."""
    valor_total = sum(
        item["preço"] * item["qtd"] 
        for item in pedido["itens"]
    )
    return {**pedido, "valor_total": valor_total}

# Executar o pipeline
pedidos_processados = list(
    map(
        calcular_valor_pedido,
        filter(lambda p: len(p["itens"]) > 0, pedidos)
    )
)

# Ordenar por valor descendente
pedidos_ordenados = sorted(pedidos_processados, key=lambda p: p["valor_total"], reverse=True)

# Exibir resultado
for pedido in pedidos_ordenados:
    print(f"Pedido {pedido['id']} - {pedido['cliente']}: R$ {pedido['valor_total']:.2f}")

# Calcular valor total de todos os pedidos com reduce
valor_total_geral = reduce(
    lambda total, p: total + p["valor_total"],
    pedidos_ordenados,
    0
)
print(f"\nValor total de pedidos processados: R$ {valor_total_geral:.2f}")

Este exemplo mostra como a programação funcional torna o código mais expressivo e fácil de entender, comparado a múltiplos loops aninhados.

Conclusão

Ao longo desta aula, exploramos as ferramentas fundamentais da programação funcional em Python. Primeiro, map() e filter() permitem transformar e selecionar dados de forma declarativa, tornando o código mais legível e menos propenso a erros de lógica impeditiva. Segundo, o módulo functools nos fornece operações poderosas como reduce() para agregações, partial() para especializar funções, e cache() para otimização, ampliando significativamente o que podemos expressar de forma funcional. Terceiro, itertools oferece geradores eficientes em memória para combinações, permutações, encadeamento e agrupamento, permitindo trabalhar com grandes volumes de dados sem overhead.

A verdadeira maestria em programação funcional vem de saber quando aplicar cada ferramenta. Nem todo código deve ser funcional — o equilibrio entre legibilidade, performance e contexto é essencial. Use essas técnicas quando elas clarificarem a intenção do código, não como um fim em si mesmo.

Referências


Artigos relacionados