O módulo functools
Nesta aula, exploramos o módulo functools da biblioteca padrão do Python, aprendendo a usar lru_cache para memoização, partial para fixar argumentos, reduce para redução de iteráveis e wraps para preservar metadados de funções decoradas. Ao final, você será capaz de aplicar essas ferramentas para escrever código mais eficiente e elegante.
O módulo functools é um dos pilares da programação funcional em Python, oferecendo ferramentas que permitem escrever código mais conciso, eficiente e expressivo. Nesta aula, vamos explorar quatro de seus recursos mais poderosos: lru_cache, partial, reduce e wraps. Cada um deles resolve problemas comuns de forma elegante, e dominá-los é essencial para qualquer programador Python que busca escrever código profissional.
Veremos como lru_cache pode acelerar funções recursivas e cálculos repetitivos, como partial simplifica chamadas de funções com muitos parâmetros, como reduce combina elementos de um iterável em um único valor, e como wraps mantém a introspecção de funções decoradas. Ao longo do caminho, apresentaremos exemplos práticos e discutiremos boas práticas para cada recurso.
lru_cache
O decorador @lru_cache implementa memoização, ou seja, armazena os resultados de chamadas de função em um cache de forma que, se a mesma função for chamada com os mesmos argumentos, o resultado é retornado imediatamente do cache, evitando recálculos. Isso é especialmente útil para funções recursivas, como o cálculo de Fibonacci, onde o mesmo subproblema é resolvido repetidamente.
Ao usar lru_cache, você pode especificar o tamanho máximo do cache com o parâmetro maxsize. Quando o cache atinge esse limite, os itens menos recentemente usados são descartados. O nome 'lru' vem de 'Least Recently Used', que é a política de descarte. Se você definir maxsize=None, o cache cresce sem limite, o que pode ser útil para funções com espaço de entradas pequeno.
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
print(fibonacci(50)) # Saída: 12586269025
O exemplo acima mostra como lru_cache torna o cálculo de Fibonacci(50) instantâneo, enquanto sem o cache seria inviável. Além de melhorar o desempenho, o decorador também fornece métodos úteis, como cache_info() e cache_clear(), que permitem inspecionar e limpar o cache, respectivamente.
partial
A função functools.partial permite criar uma nova função a partir de uma existente, fixando alguns de seus argumentos. Isso é útil quando você tem uma função com muitos parâmetros e deseja criar uma versão especializada que já tenha alguns valores predefinidos, reduzindo a repetição de código.
Por exemplo, suponha que você tenha uma função potencia(base, expoente) e queira criar funções para calcular o quadrado e o cubo. Com partial, você pode fixar o expoente e obter funções específicas. Isso melhora a legibilidade e a manutenção do código, pois evita duplicação.
from functools import partial
def potencia(base, expoente):
return base ** expoente
quadrado = partial(potencia, expoente=2)
cubo = partial(potencia, expoente=3)
print(quadrado(5)) # 25
print(cubo(5)) # 125
É importante notar que partial não avalia a função imediatamente; ele retorna um novo callable que, quando chamado, repassa os argumentos fixados junto com os novos. Os argumentos fixados podem ser posicionais ou nomeados, dando flexibilidade na criação de funções especializadas.
reduce
Diferente de map e filter, que produzem novos iteráveis, reduce é usada para combinar todos os elementos de um iterável em um único valor, aplicando uma função binária acumuladora. Ela é frequentemente usada em programação funcional para operações como soma, produto, máximo, etc.
Em Python 3, reduce foi movida para o módulo functools, pois é menos utilizada do que outras funções de ordem superior. No entanto, ainda é uma ferramenta poderosa para casos onde você precisa de uma redução personalizada. A função recebe dois argumentos: a função acumuladora e o iterável, e opcionalmente um valor inicial.
from functools import reduce
numeros = [1, 2, 3, 4, 5]
soma = reduce(lambda x, y: x + y, numeros)
print(soma) # 15
# Com valor inicial
produto = reduce(lambda x, y: x * y, numeros, 1)
print(produto) # 120
O exemplo acima mostra como reduce pode ser usada para somar e multiplicar elementos. Embora seja possível usar loops simples, reduce expressa a intenção de forma mais clara e é particularmente útil quando combinada com funções lambda em pipelines funcionais.
wraps
Ao criar decoradores, é comum que a função decorada perca seus metadados originais, como o nome, a docstring e os atributos de assinatura. O decorador @wraps do módulo functools resolve esse problema, copiando esses metadados da função original para a função wrapper, preservando a introspecção.
Sem wraps, ferramentas como help() e inspect mostram informações do wrapper em vez da função original, o que pode confundir usuários e desenvolvedores. Usar wraps é uma boa prática em qualquer decorador que você escreva.
from functools import wraps
def meu_decorador(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("Antes da chamada")
resultado = func(*args, **kwargs)
print("Depois da chamada")
return resultado
return wrapper
@meu_decorador
def saudacao(nome):
"""Retorna uma saudação."""
return f"Olá, {nome}!"
print(saudacao.__name__) # saudacao
print(saudacao.__doc__) # Retorna uma saudação.
No exemplo, sem @wraps, saudacao.__name__ seria 'wrapper', mas com wraps ele permanece 'saudacao'. Isso é essencial para manter a compatibilidade com ferramentas de documentação e depuração.
Boas Práticas e Observações Finais
Ao usar lru_cache, certifique-se de que a função seja pura (sem efeitos colaterais) e que os argumentos sejam hasháveis. Evite usá-la em funções que dependem de estado global ou de entrada/saída, pois os resultados podem ficar desatualizados.
Prefira partial quando você precisa fixar argumentos de forma clara e explícita, mas evite usá-la em excesso, pois pode tornar o código menos legível se os nomes das funções resultantes não forem descritivos.
Use reduce com moderação; em muitos casos, loops simples ou funções como sum() e max() são mais claros. No entanto, para reduções personalizadas, reduce é uma ferramenta valiosa.
Por fim, sempre aplique @wraps em seus decoradores. É uma prática simples que evita problemas de introspecção e melhora a experiência de quem usa seu código.
Referências
- Documentação oficial do módulo functools
- Documentação de lru_cache
- Documentação de partial
- Documentação de reduce
- Documentação de wraps
- Tutorial sobre functools no Real Python
- Artigo sobre functools no GeeksforGeeks
Exercícios
Usando
lru_cache, crie uma função que calcule o fatorial de um número de forma eficiente. Teste com números grandes (ex.: 100) e meça o tempo de execução.✓ Resposta:from functools import lru_cache @lru_cache(maxsize=None) def fatorial(n): if n < 2: return 1 return n * fatorial(n-1) import time inicio = time.time() print(fatorial(100)) fim = time.time() print(f"Tempo: {fim - inicio:.6f} segundos")Utilize
partialpara criar uma funçãosaudacao_com_pontuacaoque recebe um nome e um ponto de exclamação, e fixe a pontuação como '!'. Depois, chame-a com o nome "Maria".✓ Resposta:from functools import partial def saudacao(nome, pontuacao): return f"Olá, {nome}{pontuacao}" saudacao_com_pontuacao = partial(saudacao, pontuacao='!') print(saudacao_com_pontuacao("Maria")) # Olá, Maria!Escreva uma função que use
reducepara encontrar o maior número em uma lista. Teste com a lista [3, 7, 2, 9, 5].✓ Resposta:from functools import reduce def maior_numero(lista): return reduce(lambda x, y: x if x > y else y, lista) print(maior_numero([3, 7, 2, 9, 5])) # 9Crie um decorador chamado
log_chamadasque imprime o nome da função e os argumentos antes de chamá-la. Use@wrapspara preservar os metadados. Teste com uma função simples.✓ Resposta:from functools import wraps def log_chamadas(func): @wraps(func) def wrapper(*args, **kwargs): print(f"Chamando {func.__name__} com args={args} e kwargs={kwargs}") return func(*args, **kwargs) return wrapper @log_chamadas def soma(a, b): """Soma dois números.""" return a + b print(soma(2, 3)) print(soma.__name__) # soma print(soma.__doc__) # Soma dois números.Explique a diferença entre
lru_cacheepartiale dê um exemplo de quando cada um é mais adequado.✓ Resposta:lru_cacheé usado para memoização, ou seja, para armazenar resultados de funções caras e evitar recálculos. É ideal para funções puras e recursivas, como cálculo de Fibonacci ou fatorial.partialé usado para fixar argumentos de uma função, criando uma nova função com menos parâmetros. É útil quando você tem uma função genérica e precisa de especializações, como uma função de potência com expoente fixo. Por exemplo,lru_cacheé adequado para uma função que calcula o n-ésimo número primo, enquantopartialé adequado para uma função de saudação que fixa o tom de exaltação.