O módulo functools
Nesta aula, exploramos o módulo functools da biblioteca padrão do Python, que oferece ferramentas para trabalhar com funções de ordem superior. Aprendemos sobre lru_cache para memoização, partial para fixar argumentos, reduce para acumulação iterativa e wraps para preservar metadados de funções decoradas.
O módulo functools é uma das joias da biblioteca padrão do Python. Ele fornece funções e decoradores que operam em outras funções, permitindo que você escreva código mais limpo, eficiente e expressivo. Nesta aula, vamos explorar quatro ferramentas essenciais: lru_cache, partial, reduce e wraps. Cada uma resolve problemas comuns de programação, desde otimização de desempenho até a criação de funções especializadas.
Dominar o módulo functools é fundamental para qualquer programador Python que deseja escrever código idiomático e aproveitar ao máximo os recursos da linguagem. Vamos mergulhar em cada ferramenta, entender seu funcionamento interno e aplicá-la em exemplos práticos.
lru_cache
O decorador @lru_cache (Least Recently Used cache) implementa memoização, uma técnica que armazena os resultados de chamadas de função para evitar recálculos desnecessários. Quando uma função decorada é chamada com os mesmos argumentos, o resultado é retornado do cache em vez de executar a função novamente. Isso é particularmente útil para funções recursivas ou computacionalmente intensivas.
Por padrão, o cache não tem limite de tamanho, mas você pode especificar o parâmetro maxsize para limitar o número de entradas. Quando o cache atinge o limite, as entradas menos usadas são descartadas. O decorador também fornece métodos como cache_info() para estatísticas e cache_clear() para limpar o cache.
from functools import lru_cache
@lru_cache(maxsize=128)
def fib(n):
if n < 2:
return n
return fib(n-1) + fib(n-2)
print(fib(10)) # 55
print(fib.cache_info()) # CacheInfo(hits=9, misses=11, maxsize=128, currsize=11)
No exemplo acima, a função fib calcula o n-ésimo número de Fibonacci. Sem o cache, a complexidade seria exponencial; com ele, cada chamada é computada apenas uma vez. Isso demonstra como a memoização pode transformar algoritmos ineficientes em soluções viáveis.
partial
A função partial permite fixar um ou mais argumentos de uma função, criando uma nova função com esses valores predefinidos. Isso é útil quando você tem uma função genérica e deseja criar versões especializadas sem reescrever o código. A partial retorna um objeto chamável que se comporta como a função original, mas com os argumentos fixados.
É importante notar que partial funciona com argumentos posicionais e nomeados. Os argumentos fixados são passados primeiro, e os novos argumentos são adicionados no final. Isso pode causar confusão se você não estiver ciente da ordem, mas na prática é muito conveniente.
from functools import partial
def power(base, exp):
return base ** exp
square = partial(power, exp=2)
cube = partial(power, exp=3)
print(square(5)) # 25
print(cube(5)) # 125
No exemplo, criamos funções square e cube a partir de power, fixando o expoente. Isso evita duplicação de código e torna o código mais legível. partial é amplamente usado em programação funcional e em bibliotecas como multiprocessing para passar funções com argumentos fixos.
reduce
A função reduce aplica uma função de dois argumentos cumulativamente aos itens de um iterável, reduzindo-o a um único valor. Ela é parte do paradigma de programação funcional e é particularmente útil para operações de agregação, como somar, multiplicar ou encontrar o máximo. No Python 3, reduce foi movida para o módulo functools.
A assinatura é reduce(function, iterable[, initializer]). Se um inicializador for fornecido, ele é usado como o primeiro valor na redução; caso contrário, o primeiro item do iterável é usado como valor inicial. Se o iterável estiver vazio e não houver inicializador, um TypeError é levantado.
from functools import reduce
# Soma de uma lista
numbers = [1, 2, 3, 4, 5]
total = reduce(lambda x, y: x + y, numbers)
print(total) # 15
# Produto com inicializador
product = reduce(lambda x, y: x * y, numbers, 1)
print(product) # 120
Embora muitas operações de redução possam ser feitas com funções embutidas como sum() ou max(), reduce oferece flexibilidade para operações personalizadas. No entanto, em muitos casos, um loop explícito ou uma compreensão podem ser mais legíveis. Use reduce quando a operação for naturalmente associativa e o código ficar mais claro.
wraps
O decorador @wraps é usado dentro de decoradores para preservar os metadados da função original, como nome, docstring, e anotações. Quando você cria um decorador sem usar @wraps, a função decorada perde essas informações, o que pode causar problemas com introspecção e documentação.
O @wraps é aplicado ao wrapper interno e copia os atributos relevantes da função original para o wrapper. Ele também atualiza o __wrapped__, permitindo que ferramentas como inspect acessem a função original. Isso é considerado uma boa prática ao criar decoradores.
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.
print(saudacao.__wrapped__) # <function saudacao at 0x...>
Sem @wraps, saudacao.__name__ seria wrapper e a docstring seria perdida. Usar @wraps garante que a função decorada mantenha sua identidade, o que é crucial para depuração e para bibliotecas que dependem de introspecção.
Boas práticas e observações finais
Ao usar o módulo functools, lembre-se de que lru_cache não é adequado para funções com efeitos colaterais ou que dependem de estado externo, pois os resultados são armazenados em cache. Sempre defina maxsize para evitar uso excessivo de memória em aplicações de longa duração.
Use partial para criar APIs mais limpas e evitar repetição de argumentos. Para reduce, prefira funções embutidas quando existirem, mas não hesite em usá-la para operações personalizadas. E nunca se esqueça de usar @wraps em seus decoradores — é uma marca de profissionalismo.
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 do Real Python sobre functools
- GeeksforGeeks - Módulo functools
Exercícios
-
Implemente uma função
fatorial(n)que calcule o fatorial de um número inteiro não negativo usando recursão. Aplique@lru_cachee calculefatorial(10). Em seguida, chamefatorial.cache_info()e imprima o resultado.✓ Resposta:from functools import lru_cache @lru_cache(maxsize=None) def fatorial(n): if n <= 1: return 1 return n * fatorial(n-1) print(fatorial(10)) print(fatorial.cache_info()) -
Crie uma função
multiplicar(a, b)que retornaa * b. Usepartialpara criar uma funçãodobroque multiplica por 2 e uma funçãotriploque multiplica por 3. Teste-as com o argumento 7.✓ Resposta:from functools import partial def multiplicar(a, b): return a * b dobro = partial(multiplicar, 2) triplo = partial(multiplicar, 3) print(dobro(7)) # 14 print(triplo(7)) # 21 -
Dada a lista
valores = [5, 8, 2, 9, 1], usereducepara encontrar o maior valor na lista. Imprima o resultado.✓ Resposta:from functools import reduce valores = [5, 8, 2, 9, 1] maior = reduce(lambda x, y: x if x > y else y, valores) print(maior) # 9 -
Crie um decorador
log_chamadasque imprime "Chamando <nome_da_funcao>" antes de executar a função e "Função <nome> terminada" depois. Use@wrapspara preservar os metadados. Teste com uma função simples e verifique que__name__é preservado.✓ Resposta:from functools import wraps def log_chamadas(func): @wraps(func) def wrapper(*args, **kwargs): print(f"Chamando {func.__name__}") resultado = func(*args, **kwargs) print(f"Função {func.__name__} terminada") return resultado return wrapper @log_chamadas def soma(a, b): """Soma dois números.""" return a + b print(soma(3, 4)) print(soma.__name__) # soma -
Escreva uma função
contagem_regressiva(n)que imprime números de n até 1 usando recursão. Aplique@lru_cachee chame com n=5. Explique por que o cache não é apropriado neste caso.✓ Resposta:from functools import lru_cache @lru_cache(maxsize=None) def contagem_regressiva(n): if n < 1: return print(n) contagem_regressiva(n-1) contagem_regressiva(5)O cache não é apropriado porque a função tem efeitos colaterais (imprime na tela) e não retorna um valor que dependa apenas dos argumentos. Se a função fosse chamada novamente com o mesmo n, o cache retornaria o valor None sem executar a impressão, o que não é o comportamento desejado.