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

Exercícios

  1. 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")
    
  2. Utilize partial para criar uma função saudacao_com_pontuacao que 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!
    
  3. Escreva uma função que use reduce para 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]))  # 9
    
  4. Crie um decorador chamado log_chamadas que imprime o nome da função e os argumentos antes de chamá-la. Use @wraps para 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.
    
  5. Explique a diferença entre lru_cache e partial e 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, enquanto partial é adequado para uma função de saudação que fixa o tom de exaltação.