---
title: "Módulo functools do Python: lru_cache, partial, reduce e wraps"
url: "https://python.dev.br/blog/python-functools-lru-cache-partial-reduce/"
markdown_url: "https://python.dev.br/blog/python-functools-lru-cache-partial-reduce.MD"
description: "Guia prático do functools em Python: lru_cache, cache, partial, reduce, wraps e singledispatch com exemplos de API, ETL e automação em português brasileiro."
date: "2026-07-22"
author: "Equipe Python Brasil"
---

# Módulo functools do Python: lru_cache, partial, reduce e wraps

Guia prático do functools em Python: lru_cache, cache, partial, reduce, wraps e singledispatch com exemplos de API, ETL e automação em português brasileiro.


O `functools` é o módulo da biblioteca padrão do Python dedicado a **funções de ordem superior** — ferramentas que recebem ou devolvem outras funções. Ele concentra memoização (`lru_cache`, `cache`), fixação de argumentos (`partial`), agregação (`reduce`), preservação de metadados em [decoradores](/blog/decoradores-python-guia-pratico/) (`wraps`) e despacho por tipo (`singledispatch`). Se você já reescreveu o mesmo cálculo em loop, criou lambdas só para preencher um parâmetro ou perdeu o nome original de uma função decorada no stack trace, o `functools` provavelmente já tinha a peça certa.

Este guia cobre o que importa no dia a dia de quem escreve APIs, [pipelines de dados](/blog/etl-python-2026/), CLIs e automações em português brasileiro. Ele complementa [Decoradores em Python](/blog/decoradores-python-guia-pratico/), o [módulo itertools](/blog/python-itertools-iteracao-elegante/), o [módulo collections](/blog/collections-python-guia-completo/), [Context Managers com with](/blog/context-managers-python-with/) e [Geradores e Iteradores](/blog/geradores-iteradores-python/). A meta não é decorar a API inteira: é saber **quando** cada ferramenta paga o custo cognitivo.

## Por que o functools existe

Python trata funções como valores de primeira classe. Isso permite padrões poderosos — e também muita repetição. O `functools` empacota os padrões mais comuns:

1. **Memoização**: não recalcular o que já foi calculado com os mesmos argumentos.
2. **Partial application**: fixar parte dos parâmetros e reutilizar o resto.
3. **Redução**: combinar uma sequência em um único valor com uma operação binária.
4. **Decoradores corretos**: manter `__name__`, `__doc__` e assinatura da função original.
5. **Polimorfismo simples**: uma função, várias implementações por tipo do primeiro argumento.

Tudo isso sem dependência externa, em C onde importa (cache), e com comportamento estável entre versões do Python 3. Para quem vem de JavaScript (`bind`, `lodash.memoize`) ou de programação funcional, o `functools` é o canto da stdlib que fala a mesma língua.

## lru_cache: memoização com limite de memória

`lru_cache` é o motivo pelo qual muita gente abre o `functools` pela primeira vez. Ele envolve uma função pura (ou *quase* pura) e guarda o resultado de cada combinação de argumentos. Chamadas repetidas devolvem o valor em memória, sem reexecutar o corpo.

```python
from functools import lru_cache
import time

@lru_cache(maxsize=128)
def consulta_cep(cep: str) -> dict:
    """Simula uma chamada cara a uma API de CEP (ViaCEP, BrasilAPI, etc.)."""
    time.sleep(0.3)  # latência de rede
    # Em produção: return httpx.get(f"https://viacep.com.br/ws/{cep}/json/").json()
    return {"cep": cep, "cidade": "São Paulo", "uf": "SP"}

print(consulta_cep("01310-100"))  # demora ~0,3s
print(consulta_cep("01310-100"))  # instantâneo — veio do cache
print(consulta_cep.cache_info())
# CacheInfo(hits=1, misses=1, maxsize=128, currsize=1)
```

### maxsize, typed e cache_clear

- **`maxsize=128`** (padrão): mantém até 128 entradas; ao encher, remove a menos usada recentemente (Least Recently Used).
- **`maxsize=None`**: cache ilimitado — equivalente moderno a `functools.cache`.
- **`typed=True`**: trata `3` e `3.0` como chaves diferentes (útil quando a função se comporta diferente por tipo).
- **`cache_clear()`**: esvazia o cache — essencial em testes e quando a fonte de dados mudou.
- **`cache_info()`**: devolve hits, misses, tamanho atual e máximo — ótimo para [observabilidade](/blog/logging-em-python/) em produção.

```python
from functools import lru_cache

@lru_cache(maxsize=32)
def preco_produto(sku: str, tabela: str = "varejo") -> float:
    # consulta cara a banco ou planilha
    return 19.90

preco_produto("SKU-001")
preco_produto("SKU-001")
print(preco_produto.cache_info().hits)  # 1

# Depois de um reajuste de tabela:
preco_produto.cache_clear()
```

### Quando NÃO usar lru_cache

Memoização assume que o resultado depende **só** dos argumentos e que reutilizar o valor antigo é seguro. Evite em:

- Funções com efeito colateral (grava arquivo, envia e-mail, debita estoque).
- Funções que leem relógio, aleatoriedade ou estado global mutável.
- Argumentos não-hashable (`list`, `dict`, `set`). Converta para `tuple` / `frozenset` ou extraia uma chave estável.
- Resultados que **expiram** com o tempo (cotação de dólar, status de pedido) — a menos que você controle a invalidação com `cache_clear` ou um TTL próprio.

Para HTTP com retries e timeouts, combine com [httpx](/blog/python-httpx-requests-moderno/) e cacheie só o que for idempotente e barato de reter.

## cache: o atalho do Python 3.9+

A partir do Python 3.9 existe `functools.cache`, equivalente a `@lru_cache(maxsize=None)` com sintaxe mais limpa:

```python
from functools import cache

@cache
def fib(n: int) -> int:
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

print(fib(35))  # rápido graças à memoização recursiva
```

Use `cache` quando o domínio de entradas for pequeno (IDs de config, enums, caminhos de arquivo conhecidos). Use `lru_cache(maxsize=…)` quando a função pode ver milhares de chaves distintas ao longo do dia — por exemplo, CEPs consultados por um [bot de Telegram](/blog/criando-bots-telegram-python/) ou SKUs de um catálogo grande.

## partial: fixar argumentos sem lambda

`partial` cria uma nova callable a partir de uma função existente, **já preenchendo** parte dos parâmetros. O restante você passa na hora da chamada.

```python
from functools import partial
from pathlib import Path

# Abre sempre em UTF-8, modo leitura — só falta o caminho
ler_texto = partial(Path.open, mode="r", encoding="utf-8")

with ler_texto(Path("vendas/sp.csv")) as f:
    cabecalho = f.readline()
```

O padrão clássico em pipelines brasileiros: configurar um cliente HTTP ou uma função de log uma vez e reutilizar:

```python
from functools import partial
import logging

log = logging.getLogger("faturamento")
# logging.Logger.log(level, msg, *args, **kwargs)
aviso = partial(log.log, logging.WARNING)
erro = partial(log.log, logging.ERROR)

aviso("NF %s sem XML", "352607123456")
erro("Falha ao transmitir NF %s", "352607123456")
```

Outro caso de ouro: `map`, `filter` e callbacks de UI/CLI que esperam um callable de um único argumento.

```python
from functools import partial

def aplicar_desconto(preco: float, percentual: float) -> float:
    return round(preco * (1 - percentual / 100), 2)

# Lista de preços com 10% off na Black Friday
black_friday = partial(aplicar_desconto, percentual=10)
precos = [100.0, 59.9, 12.5]
print(list(map(black_friday, precos)))
# [90.0, 53.91, 11.25]
```

### partial vs lambda vs def

| Abordagem | Quando preferir |
|-----------|-----------------|
| `partial` | Só falta preencher parâmetros de uma função já nomeada |
| `lambda` | Expressão curta e descartável, sem reuso |
| `def` nomeada | Lógica com mais de uma linha, documentação ou testes próprios |

`partial` também preserva melhor a intenção em stack traces e se documenta sozinho: `partial(aplicar_desconto, percentual=10)` lê melhor do que `lambda p: aplicar_desconto(p, 10)` seis meses depois.

## reduce: agregar uma sequência a um valor

`reduce(funcao, iteravel[, inicial])` aplica `funcao` de forma cumulativa: pega o acumulador e o próximo item, devolve o novo acumulador, repete até o fim.

```python
from functools import reduce
from operator import mul, add

numeros = [2, 3, 4, 5]
print(reduce(mul, numeros, 1))  # 120 — produto
print(reduce(add, numeros, 0))  # 14  — soma (preferível: sum(numeros))
```

Onde o `reduce` brilha de verdade é em merges e operações que não têm builtin óbvio:

```python
from functools import reduce

# Mesclar vários dicionários de configuração (último vence)
configs = [
    {"host": "localhost", "port": 5432, "ssl": False},
    {"port": 5433},
    {"ssl": True, "user": "app"},
]

merged = reduce(lambda acc, d: {**acc, **d}, configs, {})
print(merged)
# {'host': 'localhost', 'port': 5433, 'ssl': True, 'user': 'app'}
```

Outro exemplo comum em [conciliação financeira](/blog/conciliacao-financeira-python/) e ETL: calcular o máximo de um campo sem materializar listas intermediárias, ou combinar conjuntos de permissões:

```python
from functools import reduce
from operator import or_

permissoes_por_papel = [
    {"ler", "listar"},
    {"ler", "escrever"},
    {"admin", "ler"},
]

todas = reduce(or_, permissoes_por_papel, set())
print(todas)  # {'ler', 'listar', 'escrever', 'admin'}
```

### reduce vs sum / loop for

- **Soma, produto simples, concatenação de strings curtas**: use `sum`, um gerador ou `"".join`.
- **Agregação binária com tipo complexo** (dict, set, objeto de domínio): `reduce` ou um `for` explícito — os dois são válidos; escolha o que a equipe lê mais rápido.
- **Precisa de índice ou de early-exit**: o `for` ganha.

Lembre-se: no Python 3 o `reduce` **não** está nos builtins; esqueceu o import e o nome some. Isso é de propósito — a linguagem empurra você para construções mais legíveis quando bastam.

## wraps: o detalhe que salva decoradores

Todo [decorador](/blog/decoradores-python-guia-pratico/) que envolve uma função sem `wraps` apaga o nome e a docstring originais. Em produção isso vira stack trace inútil e documentação quebrada.

```python
from functools import wraps
import time

def medir_tempo(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        inicio = time.perf_counter()
        try:
            return func(*args, **kwargs)
        finally:
            duracao = time.perf_counter() - inicio
            print(f"{func.__name__} levou {duracao:.3f}s")
    return wrapper

@medir_tempo
def gerar_relatorio(mes: int) -> str:
    """Gera o relatório mensal de vendas."""
    return f"relatorio-{mes:02d}.pdf"

print(gerar_relatorio.__name__)  # gerar_relatorio  (sem wraps seria 'wrapper')
print(gerar_relatorio.__doc__)   # Gera o relatório mensal de vendas.
```

`wraps` copia `__module__`, `__name__`, `__qualname__`, `__doc__`, `__annotations__` e atualiza `__dict__`. Em decoradores de [FastAPI](/blog/apis-rest-com-fastapi/), CLIs com [argparse](/blog/argparse-python-guia-completo/) ou jobs agendados, isso é higiene obrigatória — não é micro-otimização.

## singledispatch: uma função, vários tipos

`singledispatch` registra implementações diferentes conforme o **tipo do primeiro argumento**. É o polimorfismo de função avulsa — útil quando você não controla (ou não quer) uma hierarquia de classes.

```python
from functools import singledispatch
from datetime import date, datetime
from decimal import Decimal

@singledispatch
def serializar(valor):
    raise TypeError(f"tipo não suportado: {type(valor)!r}")

@serializar.register
def _(valor: date) -> str:
    return valor.isoformat()

@serializar.register
def _(valor: datetime) -> str:
    return valor.isoformat(timespec="seconds")

@serializar.register
def _(valor: Decimal) -> str:
    return format(valor, "f")

@serializar.register
def _(valor: int) -> str:
    return str(valor)

print(serializar(date(2026, 7, 22)))           # 2026-07-22
print(serializar(Decimal("19.90")))            # 19.90
print(serializar(datetime(2026, 7, 22, 10, 0)))  # 2026-07-22T10:00:00
```

Para métodos de instância existe `singledispatchmethod` (Python 3.8+), que despacha pelo tipo do segundo argumento (o primeiro é `self`). Combina bem com [dataclasses](/blog/dataclasses-python-guia-completo/) e com parsers de arquivos heterogêneos (CSV, JSON, XML) num [pipeline de ingestão](/blog/etl-python-2026/).

## total_ordering: menos boilerplate em classes comparáveis

Se sua classe define `__eq__` e **um** entre `__lt__`, `__le__`, `__gt__` ou `__ge__`, o decorator `@total_ordering` gera o restante:

```python
from functools import total_ordering

@total_ordering
class FaixaSalarial:
    def __init__(self, minimo: float, maximo: float):
        self.minimo = minimo
        self.maximo = maximo

    def __eq__(self, other):
        if not isinstance(other, FaixaSalarial):
            return NotImplemented
        return (self.minimo, self.maximo) == (other.minimo, other.maximo)

    def __lt__(self, other):
        if not isinstance(other, FaixaSalarial):
            return NotImplemented
        return self.maximo < other.minimo  # faixas disjuntas, ordenação simples

junior = FaixaSalarial(4_000, 7_000)
pleno = FaixaSalarial(8_000, 14_000)
print(junior < pleno)  # True
print(junior <= pleno)  # True — gerado pelo total_ordering
```

Útil em objetos de domínio (faixas, versões, prioridades de fila). Para estruturas genéricas de contagem e fila, continue preferindo [Counter e deque do collections](/blog/collections-python-guia-completo/).

## Receita prática: cache + partial em um client de API

Juntando as peças num fluxo realista — consultar uma API pública, cachear por endpoint e reutilizar headers:

```python
from functools import lru_cache, partial
import json
from urllib.request import Request, urlopen

def _get_json(url: str, token: str | None = None) -> dict:
    headers = {"Accept": "application/json", "User-Agent": "python.dev.br-tutorial"}
    if token:
        headers["Authorization"] = f"Bearer {token}"
    req = Request(url, headers=headers)
    with urlopen(req, timeout=15) as resp:
        return json.loads(resp.read().decode("utf-8"))

# partial fixa o token do ambiente de homologação
get_homolog = partial(_get_json, token="homolog-secret")

@lru_cache(maxsize=64)
def buscar_empresa(cnpj: str) -> dict:
    # Exemplo didático — troque pela API real (BrasilAPI, ReceitaWS, etc.)
    url = f"https://httpbin.org/get?cnpj={cnpj}"
    return get_homolog(url)

dados = buscar_empresa("00.000.000/0001-91")
print(buscar_empresa.cache_info())
```

Em produção você trocaria `urllib` por [httpx com timeouts e retries](/blog/python-httpx-requests-moderno/), guardaria o token em [variáveis de ambiente / pydantic-settings](/blog/pydantic-settings-configuracao-python/) e exporia `cache_clear` num endpoint admin ou num job noturno.

## functools + itertools + collections: o trio da stdlib

Os três módulos se completam:

| Módulo | Papel | Exemplo |
|--------|-------|---------|
| [`itertools`](/blog/python-itertools-iteracao-elegante/) | Construir e combinar iteradores lazy | `chain`, `groupby`, `islice` |
| [`collections`](/blog/collections-python-guia-completo/) | Estruturas especializadas | `Counter`, `defaultdict`, `deque` |
| `functools` | Transformar e memoizar funções | `lru_cache`, `partial`, `reduce` |

Pipeline típico de vendas por UF:

```python
from collections import Counter
from functools import partial
from itertools import groupby

def chave_uf(linha: dict) -> str:
    return linha["uf"]

ordenar_por_uf = partial(sorted, key=chave_uf)

vendas = [
    {"uf": "SP", "valor": 100},
    {"uf": "RJ", "valor": 80},
    {"uf": "SP", "valor": 50},
    {"uf": "MG", "valor": 40},
]

for uf, grupo in groupby(ordenar_por_uf(vendas), key=chave_uf):
    total = sum(item["valor"] for item in grupo)
    print(uf, total)

print(Counter(v["uf"] for v in vendas))
# Counter({'SP': 2, 'RJ': 1, 'MG': 1})
```

Quando o volume cresce, avalie [geradores](/blog/geradores-iteradores-python/) antes de materializar listas e, se o gargalo for I/O, [async/await](/blog/python-async-await/) em vez de cache cego.

## Checklist rápido

Use este mapa mental antes de importar:

1. **Mesma entrada, mesmo resultado caro?** → `lru_cache` / `cache`.
2. **Sempre passo os mesmos kwargs para uma API?** → `partial`.
3. **Preciso fundir uma sequência num valor sem builtin óbvio?** → `reduce` ou `for`.
4. **Estou escrevendo um decorador?** → `wraps` (sempre).
5. **Comportamento muda só pelo tipo do argumento?** → `singledispatch`.
6. **Classe comparável com um único operador real?** → `total_ordering`.

## Erros comuns (e como evitar)

**Cachear função impura.** Sintoma: testes flaky, dados velhos em produção. Solução: só memoize funções determinísticas; se o mundo externo muda, exponha `cache_clear` ou não use cache.

**Argumento mutável como chave.** Sintoma: `TypeError: unhashable type: 'list'`. Solução: `tuple(lista)`, `frozenset(conjunto)` ou uma chave string canônica (`json.dumps(d, sort_keys=True)` com cuidado).

**Esquecer `wraps`.** Sintoma: `__name__ == "wrapper"`, OpenAPI/FastAPI documentando o nome errado. Solução: `@wraps(func)` em todo decorador.

**`reduce` ilegível de três níveis.** Sintoma: ninguém na code review entende. Solução: extraia a função binária com `def` nomeada ou troque por um loop de 4 linhas.

**`maxsize` grande demais.** Sintoma: processo cresce sem parar. Solução: meça com `cache_info()`, defina `maxsize` realista e monitore hits/misses.

## Conclusão

O `functools` não é um módulo “avançado demais para o dia a dia” — é a caixa de ferramentas que tira ruído de código que você já escreve. `lru_cache` e `cache` cortam latência em consultas repetidas; `partial` elimina lambdas descartáveis; `reduce` resolve agregações binárias com clareza quando o builtin não basta; `wraps` é obrigatório em qualquer decorador sério; `singledispatch` organiza parsers e serializadores sem forçar hierarquia de classes.

Combine com [`itertools`](/blog/python-itertools-iteracao-elegante/), [`collections`](/blog/collections-python-guia-completo/), [pathlib](/blog/python-pathlib-manipulacao-caminhos-arquivos/) e [boas práticas Python 2026](/blog/boas-praticas-python-2026/) e você cobre a maior parte do que a stdlib oferece para código claro e eficiente — sem instalar nada. Na dúvida, comece por `@lru_cache` na função mais cara do seu script e por `@wraps` no próximo decorador que você escrever: o retorno aparece no relógio e no stack trace no mesmo dia.
