Programa
A função ` reduce() ` da biblioteca Python vem do mundo da programação funcional. Programação funcional (FP) é um jeito de programar em que os programas criam resultados aplicando funções a dados que não mudam.
Um padrão comum nesse estilo é o “fold”, que transforma uma sequência em um único resultado. Por exemplo, dobrando a lista de números [2, 4, 5, 3] sob a adição 14 através de etapas sucessivas: [2, 4, 5, 3] → [6, 5, 3] → [11, 3] → 14.
reduce() generaliza essa ideia. Ele aplica uma operação binária em um iterável até que só o resultado fique.
Neste artigo, explorarei os principais elementos do Python ( reduce() ) e darei alguns exemplos práticos. Se você precisa dar uma repassada nos conceitos básicos do Python, recomendo dar uma olhada nesses recursos:
História
Python inclui outras funções de programação funcional em Python, como map() e filter().
Construções funcionais como map() e filter() estavam no Python 1.0. Guido van Rossum não gostava deles, apontando que reduce() era difícil de analisar, e que um loop for é quase sempre mais legível. No Python 3.0, seguindo o PEP 3100, os desenvolvedores tiraram o ` reduce() ` como um built-in e colocaram ele no módulo ` functools `. Essa mudança para functools basicamente rebaixou o programa para o status de uma ferramenta de nicho.
Por que usar reduce()?
Na maioria das vezes, acho que provavelmente é melhor usar um loop ou um loop interno. Mas, o reduce() ainda é uma boa opção em alguns casos.
- Funcionam como pipelines. Faça uma série de transformações (que podem ser dinâmicas) de um jeito bem organizado.
- Dobras algébricas. Use para operações que têm valores de identidade naturais, como união de conjuntos com o conjunto vazio ou operações de máscara de bits com zero.
- Dobragem personalizada sem integração. Defina sua própria fusão de um jeito específico para o domínio quando não tiver nenhuma integrada.
- Acumuladores estruturados. Programa várias partes do estado ao mesmo tempo dentro de uma função acumuladora personalizada.
Como funciona a função reduce() do Python
Vamos ver como funciona o site reduce().
A assinatura da função básica de reduce() é
functools.reduce(function, iterable, [initializer])
A função reduce() aceita dois argumentos obrigatórios e um terceiro opcional.
function: uma função binária que diz como juntar dois elementos,iterable: a sequência ou iterável a ser reduzida, como uma lista ou tupla,initializer(opcional): um valor inicial para semear a função.
Exemplo passo a passo:
- [1, 3, 2, 7] → [4, 2, 7].
- [4, 2, 7] → [6, 7].
- [6, 7] → [13].
O resultado final é 13.
Exemplos simples de reduce()
Os exemplos a seguir mostram como usar o reduce().
from functools import reduce
numbers = [2, 4, 6]
product = reduce(lambda x, y: x * y, numbers) # ((2 x 4) x 6) = 8 x 6 = 48
min_value = reduce(lambda x, y: x if x < y else y, numbers) # 2
words = ['dog', 'cat', 'tree', 'pony']
str_concat = reduce(lambda x, y: x + y, words) # "dogcattreepony"
Inicializador
Sem um inicializador, reduce() pega o primeiro elemento do iterável como seu valor inicial. Se o iterável estiver vazio, reduce() lança uma exceção TypeError. Para deixar o código mais robusto, use um inicializador para definir o que fazer quando não tiver nada na entrada.
from functools import reduce
words = []
# Error: reduce() of empty iterable with no initial value
str_concat = reduce(lambda x, y: x + y, words)
# Correct: use empty string initializer
str_concat = reduce(lambda x, y: x + y, words, "")
Os inicializadores também geram um resultado com um contêiner vazio. Por exemplo, você pode juntar palavras numa lista simples de caracteres.
from functools import reduce
words = ['reduce', 'is', 'fun']
chars_list = reduce(lambda acc, word: acc + list(word), words, [])
print(chars_list) # ['r', 'e', 'd', 'u', 'c', 'e', 'i', 's', 'f', 'u', 'n']
Pra explorar mais o Python e a manipulação de dados, aqui estão algumas opções que eu recomendo.
- Introdução aocurso Importação de dados em Python
- Manipulação de dados em Python - programa de habilidades
- Remodelando dados com pandas em Python - folha de dicas
- Curso de Redução de Dimensionalidade em Python
- Pré-processamento de dados: Um guia completo com exemplos em Python blog
Definindo o redutor
Até agora, usamos funções lambda para definir o operador binário.
Você também pode usar operadores do módulo operator. O módulo operator tem versões de funções de operadores comuns e chamadas de método. Por exemplo, em vez de x + y, operator.add(x, y). Isso permite que você passe operadores pré-definidos (e eficientes) para um reduce() sem precisar escrever um lambda.
from functools import reduce
import operator as op
numbers = [2, 4, 6]
total = reduce(op.add, numbers, 0) # instead of reduce(lambda x,y: x + y, numbers)
Uma terceira opção é escrever uma função personalizada. Essa é uma boa opção quando não tem um operador pré-definido ou a função é muito complicada para um lambda.
Por exemplo, digamos que você queira tirar as duplicatas de uma lista, mas manter a ordem em que elas aparecem pela primeira vez. Você pode definir um redutor que acrescente um item a uma lista apenas se ele não tiver aparecido antes.
from functools import reduce
items = ['the', 'wild', 'wild', 'world', 'is', 'the', 'wide', 'world', 'is', 'the', 'world']
def dedup(acc, x):
if x not in acc: # O(n) membership test
acc.append(x)
return acc
unique = reduce(dedup, items, []) # ['the', 'wild', 'world', 'is', 'wide']
Para mais dicas sobre como eliminar duplicatas, dá uma olhada neste tutorial.
Desempenho do reduce() em Python
reduce() tem a ver com problemas de desempenho.
Sobrecarga da chamada de função
Python reduce() chama nossa função uma vez para cada elemento. Em uma lista com um milhão de itens, isso significa um milhão de chamadas de função, cada uma das quais cria um quadro, lida com argumentos e atualiza contagens de referência. Isso adiciona uma sobrecarga significativa.
Por outro lado, um built-in como sum faz uma única chamada para uma função C e faz um milhão de adições dentro do loop C. Essa diferença pode tornar as ordens de magnitude integradas mais rápidas.
Problemas de localidade do cache e eficiência da CPU
O Reduce também tem problemas com a localidade do cache e a eficiência da CPU.
Uma CPU moderna consegue fazer vários bilhões de instruções por segundo, mas o acesso à RAM é bem mais lento. Pra compensar, as CPUs modernas têmcaches d e (L1, L2, L3) que guardam dados pra acesso rápido.
Esses caches usam dois padrões.
- Localidade temporal Os dados usados recentemente provavelmente serão usados de novo.
- Localidade espacial Os dados próximos a um endereço usado recentemente provavelmente serão usados em breve.
Por outro lado, cada etapa de uma busca por ponteiros ( reduce() ) envolve a busca por ponteiros para encontrar o próximo elemento e chamadas de funções Python, o que quebra a localidade e atrasa a CPU. Funções embutidas e vetorizadas evitam esse problema ao executar loops C restritos.
Pra dar uma repassada sobre como escrever código Python eficiente e idiomático, dá uma olhada em:
- 5 dicas para escrever código idiomático em Pandas tutorial
- Escrevendo código Python eficiente curso
- Escrevendo código eficiente com pandas curso
Alternativas ao Python reduce()
Embutidos:
- Código C otimizado. Os built-ins rodam seus loops em código C otimizado, não em Python. Isso evita a sobrecarga que o
reduce()causa. Essa vantagem de velocidade aumenta com grandes entradas. - Legibilidade. Os built-ins têm nomes descritivos (
sum,min), então o que eles fazem é bem claro. Uma chamada parareduce()faz com que você analise a função que está sendo dobrada.
Loops:
- Desempenho. Os loops geralmente são mais lentos do que os built-ins, mas mais rápidos do que um
reduce(). - Legibilidade. Assim como os built-ins, os loops geralmente são mais fáceis de ler do que um
reduce(). Uma chamada parareduce()obriga você a analisar uma instrução funcional, enquanto um loop é mais Python.
itertools.accumulate()
A biblioteca Python itertools oferece um monte de iteradores eficientes, como count(), product() e combinations(). Uma função útil do itertools é itertools.accumulate(). Assim como reduce(), ele aplica uma função sobre um iterável. Mas, accumulate() guarda os valores intermediários do cálculo, não só o resultado final.
Por exemplo,
import itertools, operator
from functools import reduce
list(itertools.accumulate([1, 2, 3, 4], initial=0)) # [0, 1, 3, 6, 10]
reduce(operator.add, [1, 2, 3, 4], 0) # 10
Acumular é útil quando você precisa de totais acumulados ou mínimos/máximos. Por exemplo, você pode querer saber a temperatura máxima mês a mês.
Erros comuns ao usar reduce()
Quando você usar o reduce(), fique atento às seguintes armadilhas.
- Prefira alternativas mais simples, como built-ins ou loops. Guarde o site
reduce()para quando você realmente precisar dele. - Lidar com iteráveis vazios. Sempre use um inicializador apropriado para evitar erros em entradas vazias.
- Fica de olho nos problemas de memória. Não tente forçar o uso de um gerador de chaves (
reduce()) em situações em que um gerador ou uma abordagem de streaming seriam mais eficientes. - Evite lambdas complicadas. Use as funções do módulo `
operator` sempre que puder. Lambdas, principalmente com operações não associativas, podem prejudicar a clareza. - Prefira a clareza à esperteza.
Melhores práticas e diretrizes para reduce() em Python
Como com qualquer ferramenta, tem as melhores práticas com o reduce(). Se você decidiu que reduce() é a ferramenta certa pra usar, aqui vão algumas dicas de como usá-la.
Primeiro, projete o redutor.
- Defina o contrato em uma frase. “Junte as chaves do dicionário somando as contagens por chave.”
- Mantenha-o associativo, se possível. Isso permite que você paralelize e teste com mais facilidade.
- Identifique o seu elemento de identidade e use-o como inicializador. Por exemplo, para a soma, a identidade é 0. Para
min, émath.inf, e paraset union, éset(). Isso mantém o código robusto e livre deTypeErrors.
Mantenha a simplicidade e a legibilidade
- Uma operação simples. Sem efeitos colaterais.
- Dê um nome que descreva bem o redutor.
Documento
- Na string de documentação, anote o elemento de identidade e o comportamento de entrada vazia, a suposição de associatividade e a política de erros.
Teste
- Testes unitários em casos extremos: iterável vazio, tipos mistos, valores extremos.
- Testa a associatividade.
Monitorar o desempenho
- Compare entradas pequenas e grandes. Compare benchmarks com funções internas e loops.
- Se a velocidade é importante, pense em pré-processar, fazer em lotes e passar cálculos pesados para o NumPy ou pandas.
Aplicações avançadas e casos de uso reais para Python reduce()
Considerando as desvantagens, pode parecer que o reduce() não tem nenhum valor real. Pelo contrário, reduce() tem muitos casos de uso úteis.
- Processando estruturas aninhadas
- Operações do tipo banco de dados
- Pipelines de processamento de dados
- Aplicativos MapReduce
Processando estruturas aninhadas
O Reduce oferece uma maneira simples de percorrer estruturas de dados aninhadas, como objetos JSON, dobrando uma sequência de chaves em pesquisas sucessivas.
import json
from functools import reduce
import operator
data = json.loads('''
{
"user": {
"id": "ABC123",
"name": "Alice",
"email": "alice@example.com",
"profile": {
"address": {
"city": "San Francisco",
"zip": "94103"
},
"age": 34,
"skills": ["Python", "Data Science", "Machine Learning"]
}
}
}
''')
# Example lookups with reduce + operator.getitem
city = reduce(operator.getitem, ["user", "profile", "address", "city"], data)
print(city) # "San Francisco"
user_id = reduce(operator.getitem, ["user", "id"], data)
print(user_id) # "ABC123"
age = reduce(operator.getitem, ["user", "profile", "age"], data)
print(age) # 34
Usar reduce() faz sentido aqui. O JSON está bem aninhado: user → profile → address → city. Em vez de encadear pesquisas manualmente, represente o caminho como uma lista de chaves. Depois, usa reduce(operator.getitem, path, data) para percorrê-lo. Isso mantém o código genérico, legível e reutilizável.
Pipelines de processamento de dados
O Reduce pode conduzir pipelines de processamento de dados, passando os dados por uma sequência de transformações. Cada função cuida de uma etapa, e o pipeline é o resultado de aplicá-las na ordem certa. Aqui está um pipeline de brinquedo que pré-processa uma sequência de texto antes de alimentá-la em um modelo de NLP.
from functools import reduce
import re
# Define preprocessing steps
def strip_punctuation(s):
return re.sub(r"[^\w\s]", "", s)
def to_lower(s):
return s.lower()
def remove_stopwords(s):
stops = {"the", "is", "a", "of"}
return " ".join(word for word in s.split() if word not in stops)
def stem_words(s):
# trivial "stemmer": cut off 'ing'
return " ".join(word[:-3] if word.endswith("ing") else word for word in s.split())
pipeline = [
strip_punctuation,
to_lower,
remove_stopwords,
stem_words,
]
# Input data
text = "The quick brown fox is Jumping over a log."
# Apply pipeline with reduce
processed = reduce(lambda acc, f: f(acc), pipeline, text)
print(processed) # quick brown fox jump over log
Tratamento de erros em aplicativos complexos
Vamos voltar ao exemplo JSON aninhado. Agora, a chamada direta para reduce(operator.getitem, …) gera uma exceção KeyError ou TypeError se uma chave estiver faltando ou se encontrar um objeto que não seja um dicionário. Para deixar o código mais seguro, crie uma função auxiliar que envolva operator.getitem em um bloco try/except e retorne um valor padrão quando ocorrer um erro.
Aqui está uma possibilidade para a função auxiliar.
def deep_get(data, keys, default=None):
"""Traverse nested dicts/lists safely with reduce."""
try:
return reduce(operator.getitem, keys, data)
except (KeyError, IndexError, TypeError):
return default
Agora, mude as pesquisas de exemplo para usar nossa nova função em vez de uma função desempacotada. reduce()
# Example lookups
city = deep_get(data, ["user", "profile", "address", "city"], default="Unknown City")
print(city) # "San Francisco"
user_id = deep_get(data, ["user", "id"], default="N/A")
print(user_id) # "ABC123"
age = deep_get(data, ["user", "profile", "age"], default="N/A")
print(age) # 34
# Example with missing key
phone = deep_get(data, ["user", "profile", "phone"], default="No phone")
print(phone) # "No phone"
Transformações de dados em várias etapas com map() e filter()
Você pode juntar o reduce() com outras ferramentas úteis, como map() e filter(), para criar transformações de dados em várias etapas. Aqui está o nosso pipeline de pré-processamento de PNL anterior, escrito de forma funcional.
from functools import reduce
import re
# Define preprocessing steps
def strip_punctuation(s):
return re.sub(r"[^\w\s]", "", s)
def to_lower(s):
return " ".join(map(str.lower, s.split()))
def remove_stopwords(s):
stops = {"the", "is", "a", "of"}
return " ".join(filter(lambda w: w not in stops, s.split()))
def stem_words(s):
# trivial "stemmer": cut off 'ing'
return " ".join(map(lambda w: w[:-3] if w.endswith("ing") else w, s.split()))
# Pipeline of transformations
pipeline = [
strip_punctuation,
to_lower,
remove_stopwords,
stem_words,
]
# Input data
text = "The quick brown fox is Jumping over a log."
# Apply pipeline with reduce
processed = reduce(lambda acc, f: f(acc), pipeline, text)
print(processed) # quick brown fox jump over log
As transformações são:
- Tira a pontuação. “A rápida raposa marrom está pulando sobre um tronco.” → “A rápida raposa marrom está pulando sobre um tronco.”
- Letras minúsculas
“A rápida raposa marrom está pulando sobre um tronco” → “a rápida raposa marrom está pulando sobre um tronco”
- Tira as palavras irrelevantes. “A rápida raposa marrom está pulando sobre um tronco” → “rápida raposa marrom pulando sobre tronco”
- Palavras-raiz
“raposa marrom rápida pulando tronco” → “raposa marrom rápida pula tronco”
Para saber mais sobre programação funcional e vetorização, a gente recomenda esses artigos do DataCamp.
- Filtro Python (): Guarde o que você precisa - tutorial
- Groupby, split-apply-combine e pandas - tutorial
- Tutorial do Pandas Apply - tutorial
Integração com o ecossistema Python moderno
Pra usar bem o reduce(), é legal entender como ele se encaixa no ecossistema Python moderno. Vamos ver como ele se encaixa ao lado do NumPy e do pandas, como ele sustenta sistemas paralelos e distribuídos e como ele interage com ferramentas modernas, como analisadores estáticos.
numpy e pandas
O NumPy e o pandas funcionam a partir de código C otimizado, então não duplique suas funcionalidades com reduce(). Mas, o reduce() é uma boa opção para pipelines com etapas dinâmicas. Por exemplo, você pode compor várias transformações NumPy em uma matriz.
from functools import reduce
import numpy as np
def standardize(x):
return (x - x.mean()) / (x.std() + 1e-9)
def clip(x):
return np.clip(x, 0, 1)
def log(x):
return np.log1p(x)
x = np.array([1, 500, 40.5, 100, 250.45])
funcs = [standardize, clip, log]
y = reduce(lambda a, f: f(a), funcs, x) # x is ndarray
Estruturas de computação paralela e distribuída
A redução é essencial para estruturas de computação paralela e distribuída. Esses sistemas funcionam dividindo um conjunto de dados em partições, processando essas partições em paralelo e, em seguida, combinando esses resultados parciais em uma única resposta. A etapa de “combinar” é a redução.
Para que a redução funcione corretamente em um cluster, certifique-se de que as seguintes condições sejam atendidas.
- Associatividade. A operação deve dar o mesmo resultado, não importa como você agrupar. Isso permite que resultados parciais sejam combinados em qualquer ordem na rede.
- Elemento de identidade. A operação precisa ter um inicializador que não mexa no resultado final. Isso garante a correção quando as partições estão vazias ou têm tamanhos diferentes.
Se essas propriedades não forem mantidas, as reduções ficam lentas (porque não podem ser paralelizadas com segurança) ou incorretas (porque os resultados dependem da ordem de avaliação).
Esse processo precisa de uma operação associativa e uma identidade clara. Caso contrário, você vai acabar com um código lento ou resultados errados.
Ferramentas de análise estática
Um analisador estático (como mypy, Pyright, ruff ou bandit) é uma ferramenta que dá uma olhada no código sem precisar rodá-lo. Essas ferramentas detectam bugs, aplicam regras de estilo e verificam a correção dos tipos.
Os analisadores estáticos têm dificuldade com reduce(). No processo de dobragem, o tipo de acumulador pode ser diferente do tipo de elemento, e a inferência de tipo fica confusa.
Dá uma olhada nesse código.
from functools import reduce
def add_chars(acc, word):
acc.extend(word) # extend adds each character of the string
return acc
chars = reduce(add_chars, ["hi", "ok"], [])
print(chars) # ['h', 'i', 'o', 'k']
Mesmo que esse código funcione bem, um analisador estático pode reclamar de algumas coisas.
- O tipo de elemento do inicializador [] é meio confuso.
- Os analisadores podem presumir que os tipos acumulador e elemento são compatíveis.
Para deixar o código mais claro para humanos e analisadores, adicione dicas de tipo.
from functools import reduce
from typing import List
def add_chars(acc: List[str], word: str) -> List[str]:
acc.extend(word) # extend adds each character of the string
return acc
chars: List[str] = reduce(add_chars, ["hi", "ok"], [])
print(chars) # ['h', 'i', 'o', 'k']
Agora, o redutor mostra explicitamente
- O acumulador é um
List[str]. - Cada elemento é um
str. - O tipo de retorno é um objeto de retorno de tipo(
list[str]).
Com essas dicas, os analisadores estáticos podem verificar o código com rigor.
Conclusão
Reduce vem da programação funcional, onde juntar coleções em um único resultado é uma ideia central. Mesmo que não seja mais uma ferramenta de primeira classe no Python, ela ainda tem seu lugar. Quando você precisa de pipelines flexíveis, dobras personalizadas ou operações que não se encaixam perfeitamente nas funções existentes, o reduce() é uma ferramenta poderosa. Usado com cuidado, ele se integra perfeitamente ao ecossistema Python e continua sendo uma ferramenta prática nas situações certas.
Alguns links relacionados ao Python que podem ser úteis.
Perguntas frequentes sobre reduce() em Python
O que o reduce() faz?
Ele aplica várias vezes uma função de dois argumentos a um iterável para reduzir a um único resultado.
Onde está definido reduce()?
Antes da versão 3, era uma função integrada. Desde então, ele tá no módulo functools.
Que tipo de funções devo passar para o reduce()?
Funções simples, associativas e bem documentadas. Evite efeitos colaterais e lógica não associativa.
Como isso se compara ao itertools.accumulate()?
reduce() retorna apenas o resultado final, enquanto accumulate() mostra todos os resultados intermediários.
Quando devo usar um inicializador?
Use um inicializador quando o iterável puder estar vazio.