Função pura é aquela que sempre produz a mesma saída para uma mesma entrada.
Ela não modifica variáveis externas nem causa efeitos colaterais.
Primeiramente, funções puras são previsíveis e fáceis de testar.
Por exemplo, def soma(a, b): return a + b é pura.
Além disso, elas não dependem de estado global ou I/O.
A voz passiva é usada aqui: “os dados são transformados sem alteração externa”.
Quando utilizar funções puras? Em operações matemáticas e transformações de dados.
Também em funções de validação e cálculos repetitivos.
Python permite escrever código funcional de forma natural.
Vamos explorar características, vantagens e exemplos práticos.
Três subtítulos guiarão você pelas funções puras.
Ao final, você identificará e criará funções puras com confiança.
Características essenciais das funções puras
Função pura não tem efeitos colaterais observáveis. Ela não altera variáveis globais, arquivos ou bancos de dados. Também não modifica seus argumentos de entrada (imutabilidade). Quando usar essas características? Em código que precisa de confiabilidade. Por exemplo, sistemas financeiros ou de processamento crítico. A voz passiva é aplicada: “os argumentos são tratados como somente leitura”. Exemplo comparando funções puras e impuras:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
# FUNÇÕES PURAS def soma_pura(a, b): """Sempre retorna a+b. Sem efeitos colaterais.""" return a + b def desconto_puro(preco, percentual): """Calcula desconto sem modificar o preço original.""" return preco * (1 - percentual / 100) def media_pura(numeros): """Calcula média sem modificar a lista original.""" return sum(numeros) / len(numeros) def fatorial_puro(n): """Fatorial recursivo (puro).""" if n <= 1: return 1 return n * fatorial_puro(n - 1) # FUNÇÕES IMPURAS saldo_global = 1000 def sacar_impuro(valor): """Modifica variável global (efeito colateral).""" global saldo_global saldo_global -= valor return saldo_global def adicionar_item_impuro(lista, item): """Modifica a lista original (efeito colateral).""" lista.append(item) return lista def log_impuro(mensagem): """Escreve em arquivo (I/O).""" with open("log.txt", "a") as f: f.write(mensagem + "\n") return True # Demonstração print("=== Funções Puras ===\n") print(f"soma_pura(5, 3): {soma_pura(5, 3)}") print(f"desconto_puro(100, 10): R${desconto_puro(100, 10):.2f}") numeros = [1, 2, 3, 4, 5] print(f"media_pura({numeros}): {media_pura(numeros)}") print(f"fatorial_puro(5): {fatorial_puro(5)}") print("\n=== Funções Impuras ===") print(f"Saldo inicial: {saldo_global}") sacar_impuro(200) print(f"Após saque: {saldo_global}") lista_original = [1, 2, 3] adicionar_item_impuro(lista_original, 4) print(f"Lista original foi modificada: {lista_original}") |
Funções puras são transparentes e previsíveis. Funções impuras podem causar bugs difíceis de rastrear.
Vantagens e benefícios das funções puras
Funções puras são extremamente fáceis de testar unitariamente. Elas não dependem de contexto ou estado externo. Além disso, são seguras para execução paralela (sem locks). Quando usar essas vantagens? Em sistemas concorrentes e testes. Também em código que precisa ser reutilizado em diferentes contextos. A voz passiva é aplicada: “os testes podem ser escritos sem preparação complexa”. Exemplo demonstrando vantagens:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
from functools import lru_cache import time import threading # Função pura com cache (memoization) @lru_cache(maxsize=128) def fibonacci_puro(n): """Fibonacci puro com cache automático.""" if n < 2: return n return fibonacci_puro(n - 1) + fibonacci_puro(n - 2) # Função pura para processamento paralelo def processar_elemento_puro(x): """Processa um elemento sem dependências externas.""" return x * x - 2 * x + 1 # Demonstração de testabilidade def testar_funcao_pura(): """Teste simples sem necessidade de mocks.""" assert soma_pura(2, 2) == 4 assert soma_pura(-1, 1) == 0 assert soma_pura(0, 0) == 0 print("Todos os testes de soma_pura passaram!") print("=== Vantagens das Funções Puras ===\n") # 1. Testabilidade print("1. Testabilidade:") testar_funcao_pura() # 2. Memoization (cache) print("\n2. Memoization (cache):") inicio = time.time() print(f"fibonacci_puro(35): {fibonacci_puro(35)}") tempo1 = time.time() - inicio inicio2 = time.time() print(f"fibonacci_puro(35) (segunda vez): {fibonacci_puro(35)}") tempo2 = time.time() - inicio2 print(f"Primeira chamada: {tempo1:.4f}s, Segunda: {tempo2:.4f}s (cache)") # 3. Paralelismo seguro print("\n3. Paralelismo seguro (sem locks):") dados = list(range(1000)) def processar_em_paralelo(func, lista): """Executa função pura em paralelo (seguro).""" from concurrent.futures import ThreadPoolExecutor with ThreadPoolExecutor(max_workers=4) as executor: resultados = list(executor.map(func, lista)) return resultados resultados = processar_em_paralelo(processar_elemento_puro, dados[:10]) print(f"Processamento paralelo dos primeiros 10: {resultados}") # 4. Composição previsível print("\n4. Composição previsível:") def compor(f, g): return lambda x: f(g(x)) def mais_um(x): return x + 1 def vezes_dois(x): return x * 2 f1 = compor(mais_um, vezes_dois) f2 = compor(vezes_dois, mais_um) print(f"(mais_um ∘ vezes_dois)(5) = {f1(5)}") print(f"(vezes_dois ∘ mais_um)(5) = {f2(5)}") # 5. Referencial transparência print("\n5. Referencial transparência:") # Expressão: (soma_pura(3,4) * 2) == (7 * 2) # Podemos substituir a chamada pelo resultado print(f"soma_pura(3,4) * 2 = {soma_pura(3,4) * 2}") print(f"7 * 2 = {7 * 2}") |
Funções puras simplificam testes, cache e paralelismo. Elas são a base para código robusto e escalável.
Identificando e transformando funções impuras
Nem toda função precisa ou deve ser pura. I/O, rede, banco de dados e aleatoriedade exigem impureza. Quando identificar uma função impura? Quando ela lê arquivos ou gera números. Também quando modifica estado global ou argumentos mutáveis. A voz passiva é aplicada: “a impureza é isolada nas bordas do sistema”. Exemplo de como transformar funções impuras em puras:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
import random import datetime # === FUNÇÃO IMPURA ORIGINAL === def gerar_saudacao_impura(nome): """Usa data/hora atual (impuro).""" hora = datetime.datetime.now().hour if hora < 12: periodo = "bom dia" elif hora < 18: periodo = "boa tarde" else: periodo = "boa noite" return f"Olá {nome}, {periodo}!" # === VERSÃO PURA (recebe a hora como parâmetro) === def gerar_saudacao_pura(nome, hora): """Versão pura: depende apenas dos argumentos.""" if hora < 12: periodo = "bom dia" elif hora < 18: periodo = "boa tarde" else: periodo = "boa noite" return f"Olá {nome}, {periodo}!" # === FUNÇÃO IMPURA ORIGINAL === def sorteio_impura(): """Depende de random (impuro).""" return random.randint(1, 100) # === VERSÃO PURA (recebe semente ou valor) === def sorteio_pura(semente): """Versão pura: usa a semente para gerar número.""" import hashlib hash_valor = int(hashlib.md5(str(semente).encode()).hexdigest()[:8], 16) return (hash_valor % 100) + 1 # === FUNÇÃO IMPURA ORIGINAL === contador_global = 0 def contador_impuro(): """Modifica estado global (impuro).""" global contador_global contador_global += 1 return contador_global # === VERSÃO PURA === def contador_pura(estado): """Recebe estado atual, retorna novo estado e valor.""" novo_estado = estado + 1 return novo_estado, novo_estado # Demonstração print("=== Transformando Funções Impuras em Puras ===\n") print("Saudação original (impura):") print(gerar_saudacao_impura("Ana")) print(gerar_saudacao_impura("Ana")) # Pode mudar print("\nSaudação pura (com hora fixa):") print(gerar_saudacao_pura("Ana", 8)) print(gerar_saudacao_pura("Ana", 15)) print(gerar_saudacao_pura("Ana", 20)) print(f"\nSorteio impuro: {sorteio_impura()}, {sorteio_impura()}") print(f"Sorteio puro (semente 42): {sorteio_pura(42)}") print(f"Sorteio puro (semente 42): {sorteio_pura(42)} (mesmo resultado)") print(f"\nContador impuro: {contador_impuro()}, {contador_impuro()}") estado = 0 estado, valor1 = contador_pura(estado) estado, valor2 = contador_pura(estado) print(f"Contador puro: {valor1}, {valor2}") # Estratégia de isolamento (bordas impuras) def processar_dados(linhas): """Função pura que processa linhas.""" return [linha.strip().upper() for linha in linhas if linha.strip()] def ler_arquivo_impuro(caminho): """Função impura na borda (I/O).""" with open(caminho, 'r') as f: return f.readlines() # Exemplo com arquivo (cria para demonstração) with open("teste_puro.txt", "w") as f: f.write("linha 1\n") f.write("\n") f.write("linha 2\n") f.write(" linha com espaços \n") # Borda impura + núcleo puro linhas_impuras = ler_arquivo_impuro("teste_puro.txt") resultado_puro = processar_dados(linhas_impuras) print(f"\nArquivo processado: {resultado_puro}") |
Isolando impurezas nas bordas do sistema, o núcleo fica puro. A fórmula da pureza de uma função pode ser expressa: \(P = \frac{\text{mesma saída para mesma entrada}}{\text{sem efeitos colaterais}}\) Funções puras não são bala de prata para tudo. Para I/O e interação com usuário, impureza é necessária. Porém, maximize funções puras no núcleo da sua lógica. Seu código será mais fácil de entender, testar e manter.