Funções — uma explicação que eu gostaria de ter recebido
Eu passei uns seis meses sem entender por que meu código Python entrava em loop infinito durante um deploy de API. O problema era uma função que eu havia escrito para buscar dados de um endpoint externo, mas que retornava None quando a requisição falhava, e eu estava tratando isso como se fosse um string vazio. O resultado era um loop que jamais terminava. Quando finalmente identifiquei a causa — a função não tinha raise nem tratamento de exceção — a solução foi simples: adicionar um if response.status_code != 200: raise HTTPError. Isso me custou uma noite inteira e o deploy atrasou 4 horas.
O que são funções e por que eu as uso
Funções são blocos de código reutilizáveis que recebem entradas, processam e devolvem saídas. Nada mais, nada menos. No começo eu achava que precisava explicar funções com metáforas bonitas sobre "caixas mágicas" ou "receitas de bolo". Não precisa. Funções existem porque copiar e colar código é doloroso e gera bugs. Uma vez eu copiei uma lógica de cálculo de desconto para três lugares diferentes no mesmo projeto. Quando mudamos a regra do desconto de 10% para 15%, atualizei dois e esqueci o terceiro. O cliente pagou a menos e reclamou. Eu levei uma semana para rastrear onde estava o erro. O conceito é simples: você define uma função uma vez, chama ela quantas vezes precisar, e o código é executado no mesmo contexto. Variáveis locais dentro da função não poluem o resto do programa. Parâmetros controlam o que entra. O return define o que sai. def no Python, function no JavaScript, func no Go. A sintaxe muda, a ideia não.
Eu já vi desenvolvedores júnior acharem que funções servem só para evitar repetição. Existe um erro aí. Funções também servem para isolar responsabilidade, testar unidades isoladamente, e criar abstração limpa. Sem função, seu código vira um spaghetti onde qualquer bug exige ler tudo do início ao fim.
Como criar uma função passo a passo
Vou mostrar como eu escrevo funções hoje, depois explicar alguns conceitos. Primeiro, a estrutura básica no Python:
def calcular_desconto(preco, porcentagem):
return preco * (1 - porcentagem / 100)
Isso é tudo. Você começa com def, o nome da função, parênteses com parâmetros, dois pontos, e o corpo indentado. O return devolve o valor. Sem return, a função retorna None implicitamente. Isso é um erro comum que gera bugs chatos. Agora no JavaScript:
function calcularDesconto(preco, porcentagem) {
return preco * (1 - porcentagem / 100);
}
Sintaxe diferente, mesma ideia. O arrow function no ES6 é mais curto:
const calcularDesconto = (preco, porcentagem) => preco * (1 - porcentagem / 100);
Nota importante: arrow functions não têm seu próprio this. Isso quebra código que depende do contexto de chamada. Eu perdi uma tarde debugando isso em uma aplicação React. O this ficava undefined e o estado não atualizava. Use bind ou uma função normal se precisar de this.
Tipos de parâmetros e armadilhas
Parâmetros podem ser posicionais, nomeados, com valor padrão, variádicos, keyword-only. No Python:
def conectar(host, port=80, timeout=30, *, ssl=False):
...
O * antes de ssl força o usuário a passar por nome. Isso evita bugs onde a ordem dos argumentos importa. Eu vi código legado aceitar conectar("localhost", True, 80) esperando um boolean, mas receber um int. A conexão falhava silenciosamente. A correção foi transformar em keyword-only e adicionar validação explícita. Valores padrão são avaliados uma vez na definição, não a cada chamada. Isso é um erro que gera bugs difíceis de rastrear. Se você usar uma lista como valor padrão:
def append_item(item, lista=[]):
lista.append(item)
return lista
A mesma lista é reutilizada entre chamadas. O primeiro caller modifica e o segundo recebe a lista já populada. O workaround é usar None como padrão:
def append_item(item, lista=None):
if lista is None:
lista = []
lista.append(item)
return lista
Isso me custou duas noites debugando. O bug aparecia só em produção, quando o cache de funções era compartilhado entre threads.
👉 Clique no botão abaixo para saber mais sobre o assunto!
Escopo e closures — onde eu errei
Funções criam escopo local. Variáveis definidas dentro não vazam. Isso é útil e seguro. Mas closures podem gerar surpresas. Eu escrevi uma função que retornava outra função, e o loop externo já tinha terminado quando a inner era chamada. O resultado era sempre o último valor do loop, não o esperado. O workaround foi capturar o valor com parâmetro padrão:
funcoes = []
for i in range(5):
funcoes.append(lambda x, i=i: i + x)
print(funcoes[2](10)) 12, não 10
O i=i captura o valor atual de i. Sem isso, o closure referencia a variável, que já foi modificada pelo loop. Isso é um erro que gera bugs intermitentes, difíceis de reproduzir.
Recursão — quando usar e quando fugir
Funções podem chamar a si mesmas. Isso é recursão. Útil para árvores, grafos, divide et impera. Mas tem custo: cada chamada empilha na stack. Python limita a 1000 recursões por padrão. Eu escrevi uma função recursiva para percorrer um diretório com 1500 níveis. Ela travou com RecursionError. O workaround foi transformar em iterativo com uma pilha explícita:
def listar_arquivos(caminho):
pilha = [caminho]
while pilha:
atual = pilha.pop()
print(atual)
pilha.extend(subdiretorios(atual))
Isso cortou o tempo de execução de 3 segundos para 0.02 segundos, dependendo do sistema de arquivos. Recursão é elegante, mas não é bala de prata.
Performance e otimizações
Chamar uma função tem custo: alocação de frame na stack, passagem de parâmetros, retorno. Em Python, chamadas de função são até 10x mais lentas que operações inline em loops quentes. Eu otimizei um trecho que calculava média de milhões de valores. A função media() era chamada a cada iteração. Inlinear o cálculo cortou o tempo de 45 segundos para 8 segundos. Perfilador (cProfile) mostrou que 70% do tempo era gasto em chamadas de função. Funções embutidas (map, filter, reduce) são mais rápidas que loops equivalentes em Python porque são implementadas em C. Eu substituí um loop com append por map e ganhei 30% de performance. Mas legibilidade caiu. Trade-off real: velocidade versus clareza.
Testes unitários e funções puras
Funções puras não têm efeito colateral: mesmo input produz mesmo output. Isso facilita testes. Eu escrevi uma função que lia arquivo, processava dados e gravava log. Era impossível testar isoladamente. O workaround foi separar em três funções: ler_arquivo(), processar(), gravar_log(). Cada uma testável separadamente. Tempo de teste caiu de 2 horas para 15 minutos. Funções impuras são necessárias — IO, estado global, randômico. Mas isole-as. Chame funções puras delas, não o contrário. Isso é um erro que gera código acoplado, impossível de testar.
Erros comuns que eu vejo todo dia
Primeiro: confundir mutáveis com imutáveis. Strings, números, tuplas são imutáveis. Listas, dicionários, sets são mutáveis. Passar uma lista como parâmetro e modificar dentro da função altera o original. Eu vi código que "resetava" uma lista passando uma cópia rasa, mas a cópia compartilhava referências a objetos mutáveis. Modificação interna vazava para fora. A correção foi usar copy.deepcopy(). Segundo: esquecer que return interrompe a execução. Código após return nunca roda. Eu escrevi uma função com return no meio e lógica de cleanup depois. O cleanup não executava em erro. O workaround foi usar try/finally ou context manager.
Terceiro: aninhar funções demais. Cada nível de indireção custa legibilidade. Eu tenho uma função que chamava outra que chamava mais outra, e nenhuma documentava o que fazia. O resultado era um quebra-cabeça. Reescrevi em três funções claras com nomes autoexplicativos. Tempo de onboarding de novo desenvolvedor caiu de 3 dias para 4 horas.
Bibliotecas e funções embutidas
Python tem centenas de funções embutidas: len, max, sorted, enumerate, zip. Uso diário. JavaScript também: Array.prototype.map, reduce, filter, find. Conhecer a biblioteca padrão economiza horas de reinventar roda. Eu já vi desenvolvedores escreverem my_sort() quando sorted() resolve. Perda de tempo real. Bibliotecas de terceiros estendem funções: numpy.dot para multiplicação de matrizes, pandas.apply para operações em DataFrames. Uso pesado em ciência de dados. Mas cuidado: apply em pandas é 10x mais lento que operações vetorizadas. Perfilador mostra onde a função está gargalo.
Download e recursos adicionais
Não existe download de funções — são construídas, não instaladas. Mas recomendo ferramentas: black para formatar Python automaticamente, eslint para JavaScript, mypy para tipagem estática. Tempo de review de código cai de 1 hora para 15 minutos com formatação automática. Livro recomendado: "Fluent Python" de Luciano Ramalho, capítulos 7 e 8 sobre funções e closures. Explica os mecanismos internos do Python com exemplos práticos. Eu li na primeira semana e entendi porque meu código travava. Economizou dias de debugging.
Quando funções falham
Funções não resolvem tudo. Código altamente dinâmico, gerado em tempo de execução, pode não se encaixar em funções tradicionais. Meta-programação, macros, eval — existem por necessidade, não por preguiça. Eu já usei eval() para parsear configuração gerada por outra linguagem. Funcionou, mas ficou inseguro. O workaround foi substituir por parser específico com ast. Performance crítica: funções inlineadas por compilador (C, Rust) podem ser 2x mais rápidas que chamadas de função. Mas legibilidade cai. Decisão real: velocidade versus manutenção. Em maioria dos casos, funções normais são suficientes. Perfilador decide onde otimizar.
Funções com muitos parâmetros (>5) são sinal de mau design. Extraia em objeto ou struct. Eu tive uma função com 8 parâmetros que mudava a cada release. O refatoramento em classe reduziu 40% dos bugs de integração. Custo de manutenção caiu drasticamente.