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.