Programação funcional em Python

1. Introdução à Programação Funcional no Contexto Python

A programação funcional é um paradigma que trata a computação como avaliação de funções matemáticas, evitando mudanças de estado e dados mutáveis. Python, embora seja uma linguagem multiparadigma, oferece suporte robusto a conceitos funcionais, permitindo que desenvolvedores combinem o melhor de diferentes abordagens.

Diferentemente da programação imperativa (focada em sequências de instruções que modificam estado) e da orientação a objetos (centrada em objetos que encapsulam estado e comportamento), a programação funcional enfatiza:

  • Funções puras: sem efeitos colaterais
  • Imutabilidade: dados que não mudam após criação
  • Composição: construção de soluções complexas a partir de funções simples

Em Python, funções são cidadãos de primeira classe: podem ser atribuídas a variáveis, passadas como argumentos e retornadas de outras funções. Isso é a base para todo o paradigma funcional na linguagem.

# Função como cidadão de primeira classe
def saudacao(nome):
    return f"Olá, {nome}!"

minha_funcao = saudacao
print(minha_funcao("Maria"))  # Olá, Maria!

# Função como argumento
def executar(f, valor):
    return f(valor)

print(executar(saudacao, "João"))  # Olá, João!

2. Funções Puras e Imutabilidade

Uma função pura é determinística (mesma entrada → mesma saída) e não produz efeitos colaterais (não modifica variáveis externas, não faz I/O, não altera argumentos).

# Função pura - determinística e sem efeitos colaterais
def somar(a, b):
    return a + b

# Função impura - depende de estado externo
total = 0
def adicionar_ao_total(valor):
    global total
    total += valor
    return total

Para manter a imutabilidade, Python oferece estruturas como tuple, frozenset e str. Ao trabalhar com listas e dicionários, prefira criar cópias:

from copy import deepcopy

# Evitando mutação
def adicionar_item(lista_original, item):
    """Retorna nova lista sem modificar a original"""
    return lista_original + [item]  # Cria nova lista

dados = [1, 2, 3]
novos_dados = adicionar_item(dados, 4)
print(dados)       # [1, 2, 3] - original intacta
print(novos_dados) # [1, 2, 3, 4]

# deepcopy para estruturas aninhadas
config = {"nivel": 1, "opcoes": {"som": True}}
config_copia = deepcopy(config)
config_copia["opcoes"]["som"] = False
print(config["opcoes"]["som"])  # True - original preservada

3. Funções de Alta Ordem: map, filter e reduce

map()

Aplica uma função a cada elemento de um iterável, retornando um iterador.

numeros = [1, 2, 3, 4, 5]

# Com map
quadrados = list(map(lambda x: x ** 2, numeros))
print(quadrados)  # [1, 4, 9, 16, 25]

# Equivalente com list comprehension
quadrados_lc = [x ** 2 for x in numeros]

# Múltiplos iteráveis
def somar(a, b):
    return a + b

resultado = list(map(somar, [1, 2, 3], [10, 20, 30]))
print(resultado)  # [11, 22, 33]

filter()

Seleciona elementos que satisfazem uma condição.

numeros = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

pares = list(filter(lambda x: x % 2 == 0, numeros))
print(pares)  # [2, 4, 6, 8, 10]

# Com função nomeada
def maior_que_cinco(x):
    return x > 5

selecionados = list(filter(maior_que_cinco, numeros))
print(selecionados)  # [6, 7, 8, 9, 10]

reduce()

Do módulo functools, acumula sequencialmente os elementos.

from functools import reduce

numeros = [1, 2, 3, 4, 5]

# Soma acumulada
soma = reduce(lambda a, b: a + b, numeros)
print(soma)  # 15

# Produto acumulado
produto = reduce(lambda a, b: a * b, numeros)
print(produto)  # 120

# Com valor inicial
soma_inicial = reduce(lambda a, b: a + b, numeros, 100)
print(soma_inicial)  # 115

4. Expressões Lambda e Funções Anônimas

Lambda são funções anônimas de uma única expressão, ideais para operações simples.

# Sintaxe: lambda argumentos: expressão
dobro = lambda x: x * 2
print(dobro(5))  # 10

# Uso com sorted
pessoas = [("Ana", 25), ("João", 30), ("Maria", 20)]
ordenado = sorted(pessoas, key=lambda pessoa: pessoa[1])
print(ordenado)  # [('Maria', 20), ('Ana', 25), ('João', 30)]

# Evite lambda para lógica complexa
# Ruim:
calcular = lambda x, y: x ** 2 + y * 3 - (x + y) ** 0.5

# Melhor:
def calcular(x, y):
    return x ** 2 + y * 3 - (x + y) ** 0.5

5. Ferramentas do Módulo itertools

O módulo itertools fornece iteradores eficientes para composição funcional.

from itertools import count, cycle, repeat, product, combinations, groupby, islice, chain

# Iteradores infinitos
contador = count(start=10, step=2)
print(list(islice(contador, 5)))  # [10, 12, 14, 16, 18]

# Combinatória
letras = ['A', 'B', 'C']
combinacoes = list(combinations(letras, 2))
print(combinacoes)  # [('A', 'B'), ('A', 'C'), ('B', 'C')]

# Pipeline funcional com itertools
dados = [1, 2, 2, 3, 3, 3, 4, 5, 5]
agrupado = groupby(sorted(dados))
resultado = {k: len(list(v)) for k, v in agrupado}
print(resultado)  # {1: 1, 2: 2, 3: 3, 4: 1, 5: 2}

# chain para concatenar iteráveis
lista1 = [1, 2, 3]
lista2 = [4, 5, 6]
concatenado = list(chain(lista1, lista2))
print(concatenado)  # [1, 2, 3, 4, 5, 6]

6. functools: Decoradores e Utilitários Funcionais

from functools import partial, lru_cache, singledispatch, reduce

# partial - fixa argumentos
def potencia(base, expoente):
    return base ** expoente

quadrado = partial(potencia, expoente=2)
cubo = partial(potencia, expoente=3)

print(quadrado(5))  # 25
print(cubo(3))      # 27

# lru_cache - memoização automática
@lru_cache(maxsize=128)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

print(fibonacci(100))  # 354224848179261915075 (rápido!)

# singledispatch - despacho polimórfico
@singledispatch
def processar(valor):
    raise NotImplementedError(f"Tipo não suportado: {type(valor)}")

@processar.register(int)
def _(valor):
    return f"Inteiro: {valor * 2}"

@processar.register(str)
def _(valor):
    return f"String: {valor.upper()}"

print(processar(5))      # Inteiro: 10
print(processar("ola"))  # String: OLA

# Composição de funções (implementação caseira)
def compor(*funcoes):
    def aplicar(x):
        resultado = x
        for f in reversed(funcoes):
            resultado = f(resultado)
        return resultado
    return aplicar

dobrar = lambda x: x * 2
incrementar = lambda x: x + 1
funcao_composta = compor(dobrar, incrementar)
print(funcao_composta(5))  # (5 + 1) * 2 = 12

7. Generators e Lazy Evaluation

Generators permitem avaliação preguiçosa, processando dados sob demanda.

# Função geradora
def fibonacci_ate(n):
    a, b = 0, 1
    while a < n:
        yield a
        a, b = b, a + b

for num in fibonacci_ate(100):
    print(num, end=" ")  # 0 1 1 2 3 5 8 13 21 34 55 89

# Expressão geradora vs list comprehension
numeros = range(1000000)

# List comprehension - consome memória imediatamente
quadrados_lista = [x**2 for x in numeros]  # ~8MB em memória

# Expressão geradora - sob demanda
quadrados_gen = (x**2 for x in numeros)  # ~56 bytes

# Pipeline lazy com itertools
from itertools import islice, takewhile

def processar_stream(dados):
    pipeline = (x ** 2 for x in dados)
    pipeline = (x for x in pipeline if x % 2 == 0)
    pipeline = (x / 2 for x in pipeline)
    return pipeline

stream = processar_stream(range(100))
resultados = list(islice(stream, 5))
print(resultados)  # [0, 2, 8, 18, 32]

8. Casos de Uso Práticos e Boas Práticas

Exemplo de ETL funcional

from functools import reduce
from itertools import groupby

# Dados brutos
transacoes = [
    {"id": 1, "categoria": "alimentacao", "valor": 50.0, "data": "2024-01-01"},
    {"id": 2, "categoria": "transporte", "valor": 30.0, "data": "2024-01-01"},
    {"id": 3, "categoria": "alimentacao", "valor": 80.0, "data": "2024-01-02"},
    {"id": 4, "categoria": "lazer", "valor": 100.0, "data": "2024-01-02"},
]

# Pipeline funcional
def extrair_validos(dados):
    return filter(lambda t: t["valor"] > 0, dados)

def agrupar_por_categoria(dados):
    sorted_data = sorted(dados, key=lambda t: t["categoria"])
    return groupby(sorted_data, key=lambda t: t["categoria"])

def somar_por_grupo(grupos):
    return {
        cat: reduce(lambda acc, t: acc + t["valor"], trans, 0)
        for cat, trans in grupos
    }

# Execução do pipeline
resultado = somar_por_grupo(agrupar_por_categoria(extrair_validos(transacoes)))
print(resultado)  # {'alimentacao': 130.0, 'lazer': 100.0, 'transporte': 30.0}

Boas práticas

  • Use programação funcional para transformações de dados claras e previsíveis
  • Evite overengineering: nem tudo precisa ser funcional
  • Misture paradigmas: use classes para estado complexo e funções puras para lógica de negócio
  • Considere performance: lambdas e funções têm overhead; para loops críticos, list comprehensions ou loops tradicionais podem ser mais rápidos
# Exemplo de mistura de paradigmas
class ProcessadorDados:
    def __init__(self, dados):
        self.dados = dados  # Estado encapsulado

    def transformar(self, funcao):
        # Função pura para transformação
        return list(map(funcao, self.dados))

    def filtrar(self, condicao):
        return list(filter(condicao, self.dados))

processador = ProcessadorDados([1, 2, 3, 4, 5])
resultado = processador.transformar(lambda x: x * 2)
print(resultado)  # [2, 4, 6, 8, 10]

Referências