Este artigo trata da principal ferramenta de organização de código em projetos de IA: a função. Vamos ver como defini-las, como o Python resolve o escopo das variáveis, quais tipos de parâmetro existem e quando a recursão é a estrutura certa para o problema.
A onipresença das funções em Machine Learning e Deep Learning não é coincidência. Elas aparecem no pré-processamento dos dados, na avaliação de modelos — acurácia, F1 e companhia —, no ciclo de treinamento com os clássicos train() e evaluate(), nas ativações de redes neurais como ReLU, sigmoid e softmax, e nas funções de perda como MSE e Cross-Entropy. Saber escrevê-las bem estruturadas, com parâmetros claros e responsabilidade única, é o que separa um script experimental de código que aguenta produção.
A definição usa a palavra-chave def, seguida do nome, dos parênteses com os parâmetros e de dois pontos, com o corpo indentado. Na forma mais enxuta, sem parâmetro nenhum:
def saudacao():
print("Olá! Bem-vindo ao curso de IA.")
saudacao()
Saída:
Olá! Bem-vindo ao curso de IA.
Parâmetros tornam a função reutilizável, permitindo que o mesmo código atenda entradas diferentes:
def saudar_pessoa(nome):
print(f"Olá, {nome}! Bem-vindo ao curso de IA.")
saudar_pessoa("Ana")
saudar_pessoa("Carlos")
Saída:
Olá, Ana! Bem-vindo ao curso de IA.
Olá, Carlos! Bem-vindo ao curso de IA.
Na maioria das vezes, porém, você quer que a função devolva um resultado em vez de imprimi-lo — é o papel do return:
def calcular_imc(peso, altura):
imc = peso / (altura ** 2)
return imc
resultado = calcular_imc(70, 1.75)
print(f"IMC: {resultado:.2f}")
Saída:
IMC: 22.86
Uma conveniência que o Python oferece e que economiza muito código: devolver vários valores de uma vez, empacotados como tupla e desempacotados na atribuição.
def estatisticas(numeros):
media = sum(numeros) / len(numeros)
minimo = min(numeros)
maximo = max(numeros)
return media, minimo, maximo
notas = [7.5, 8.0, 6.5, 9.0, 7.0]
media, minimo, maximo = estatisticas(notas)
print(f"Média: {media:.2f}")
print(f"Mínimo: {minimo}")
print(f"Máximo: {maximo}")
Saída:
Média: 7.60
Mínimo: 6.5
Máximo: 9.0
Os parâmetros têm mais variedade do que parece à primeira vista, e vale conhecer cada tipo. Os posicionais são os mais simples, e neles a ordem é tudo:
def subtrair(a, b):
return a - b
print(subtrair(10, 3)) # 7
print(subtrair(3, 10)) # -7 — ordem importa!
Atribuir um valor padrão torna o parâmetro opcional na chamada. A única regra é que parâmetros com default venham depois dos que não têm:
def saudacao(nome, idioma="pt"):
if idioma == "pt":
return f"Olá, {nome}!"
elif idioma == "en":
return f"Hello, {nome}!"
elif idioma == "es":
return f"¡Hola, {nome}!"
print(saudacao("Ana")) # usa o default "pt"
print(saudacao("Bob", "en"))
print(saudacao("Carlos", "es"))
Saída:
Olá, Ana!
Hello, Bob!
¡Hola, Carlos!
Esse recurso é a espinha dorsal das APIs de ML, onde hiperparâmetros ganham valores sensatos por padrão e você sobrescreve apenas o que interessa:
def treinar_modelo(dados, learning_rate=0.01, epocas=100, verbose=True):
print(f"Treinando com lr={learning_rate}, épocas={epocas}")
# lógica de treinamento aqui...
treinar_modelo(dados=None) # usa todos os defaults
treinar_modelo(dados=None, learning_rate=0.001) # muda só o lr
treinar_modelo(dados=None, epocas=500, verbose=False)
Passar argumentos pelo nome, e não pela posição, deixa a chamada autoexplicativa e libera você da ordem original:
def criar_modelo(camadas, neuronios, ativacao):
print(f"Modelo: {camadas} camadas, {neuronios} neurônios, ativação {ativacao}")
# Chamadas equivalentes
criar_modelo(3, 128, "relu")
criar_modelo(camadas=3, neuronios=128, ativacao="relu")
criar_modelo(ativacao="relu", camadas=3, neuronios=128) # ordem não importa com nomes
Quando o número de argumentos é desconhecido de antemão, *args recolhe todos os posicionais numa tupla:
def somar_tudo(*numeros):
print(f"Argumentos recebidos: {numeros}")
return sum(numeros)
print(somar_tudo(1, 2, 3))
print(somar_tudo(10, 20, 30, 40, 50))
Saída:
Argumentos recebidos: (1, 2, 3)
6
Argumentos recebidos: (10, 20, 30, 40, 50)
150
Seu equivalente para argumentos nomeados é **kwargs, que os entrega como dicionário. É o mecanismo por trás das configurações flexíveis dos frameworks de ML:
def configurar_modelo(**kwargs):
print("Configurações recebidas:")
for chave, valor in kwargs.items():
print(f" {chave}: {valor}")
configurar_modelo(
learning_rate=0.001,
batch_size=32,
optimizer="adam",
dropout=0.5
)
Saída:
Configurações recebidas:
learning_rate: 0.001
batch_size: 32
optimizer: adam
dropout: 0.5
Todos podem coexistir na mesma assinatura, desde que respeitada a ordem: primeiro os posicionais, depois *args, então os que têm valor padrão e por último **kwargs.
def funcao_completa(obrigatorio, *args, opcional="padrão", **kwargs):
print(f"Obrigatório: {obrigatorio}")
print(f"Args extras: {args}")
print(f"Opcional: {opcional}")
print(f"Kwargs: {kwargs}")
funcao_completa("valor", 1, 2, 3, opcional="custom", chave="valor_extra")
Saída:
Obrigatório: valor
Args extras: (1, 2, 3)
Opcional: custom
Kwargs: {'chave': 'valor_extra'}
Definida a função, surge a questão de onde suas variáveis existem. Escopo é justamente isso — onde um nome pode ser acessado — e o Python resolve a busca pela regra LEGB, procurando na ordem Local, Enclosing, Global e Built-in.
No escopo local, variáveis criadas dentro da função só existem enquanto ela roda, e tentar alcançá-las de fora resulta em erro:
def calcular():
resultado = 42 # variável local
print(resultado)
calcular()
print(resultado) # NameError: name 'resultado' is not defined
Já as variáveis criadas fora de qualquer função são globais e podem ser lidas de dentro delas sem cerimônia:
taxa = 0.1 # variável global
def calcular_imposto(valor):
return valor * taxa # lê a variável global
print(calcular_imposto(1000)) # 100.0
Ler é uma coisa, modificar é outra: para alterar uma global de dentro da função é preciso declará-la com global. Use com parcimônia, porque dependência de estado global é o tipo de coisa que dificulta testes e manutenção:
contador = 0
def incrementar():
global contador
contador += 1
incrementar()
incrementar()
incrementar()
print(contador) # 3
O "E" do LEGB aparece quando uma função é definida dentro de outra: a interna enxerga o escopo da externa.
def externa():
mensagem = "Olá do escopo externo"
def interna():
print(mensagem) # acessa a variável da função externa
interna()
externa()
Saída:
Olá do escopo externo
E, do mesmo modo que global libera a escrita no escopo global, nonlocal libera a escrita no escopo da função que envolve — padrão que permite construir contadores com estado próprio:
def contador_fabrica():
contagem = 0
def incrementar():
nonlocal contagem
contagem += 1
return contagem
return incrementar
contador = contador_fabrica()
print(contador()) # 1
print(contador()) # 2
print(contador()) # 3
Esse último exemplo revela algo importante: em Python, funções são objetos como quaisquer outros. Você pode atribuí-las a variáveis, passá-las como argumento e devolvê-las de outras funções. É exatamente por isso que os frameworks de ML conseguem receber funções de ativação e de perda como parâmetro.
Atribuir a uma variável é tão simples quanto omitir os parênteses — você guarda a função, não o resultado dela:
def quadrado(x):
return x ** 2
operacao = quadrado
print(operacao(5)) # 25
Passá-la adiante como argumento abre o caminho para código genérico, em que o comportamento é decidido por quem chama:
def aplicar(funcao, valor):
return funcao(valor)
def dobrar(x):
return x * 2
def quadrado(x):
return x ** 2
print(aplicar(dobrar, 5)) # 10
print(aplicar(quadrado, 5)) # 25
Levando a ideia para o terreno de redes neurais, é assim que uma mesma rotina aplica ativações diferentes ao mesmo conjunto de entradas:
import math
def relu(x):
return max(0, x)
def sigmoid(x):
return 1 / (1 + math.exp(-x))
def aplicar_ativacao(valores, funcao_ativacao):
return [funcao_ativacao(v) for v in valores]
entradas = [-2.0, -1.0, 0.0, 1.0, 2.0]
print("ReLU: ", aplicar_ativacao(entradas, relu))
print("Sigmoid:", [round(v, 4) for v in aplicar_ativacao(entradas, sigmoid)])
Saída:
ReLU: [0, 0, 0, 1.0, 2.0]
Sigmoid: [0.1192, 0.2689, 0.5, 0.7311, 0.8808]
Quando a função é tão pequena que nomeá-la formalmente parece exagero, existe a lambda — uma função anônima de uma linha, escrita como lambda parametros: expressao:
# Sintaxe: lambda parametros: expressao
quadrado = lambda x: x ** 2
print(quadrado(5)) # 25
soma = lambda a, b: a + b
print(soma(3, 4)) # 7
Seu uso mais frequente é como argumento de sorted(), map() e filter(). Ordenando dicionários por um campo, por exemplo:
alunos = [
{"nome": "Ana", "nota": 8.5},
{"nome": "Bruno", "nota": 6.0},
{"nome": "Carla", "nota": 9.2},
]
# Ordenar por nota usando lambda
alunos_ordenados = sorted(alunos, key=lambda a: a["nota"], reverse=True)
for aluno in alunos_ordenados:
print(f"{aluno['nome']}: {aluno['nota']}")
Saída:
Carla: 9.2
Ana: 8.5
Bruno: 6.0
Com map() você transforma cada elemento; com filter(), seleciona os que interessam:
numeros = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# map: aplica uma função a cada elemento
dobrados = list(map(lambda x: x * 2, numeros))
print(dobrados)
# filter: filtra elementos pela condição
pares = list(filter(lambda x: x % 2 == 0, numeros))
print(pares)
Saída:
[2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
[2, 4, 6, 8, 10]
Funções que outras pessoas vão usar merecem documentação, e o lugar dela é a docstring: uma string logo abaixo da assinatura, acessível via help() e lida por ferramentas de documentação automática.
def calcular_acuracia(predicoes, rotulos_reais):
"""
Calcula a acurácia de um modelo de classificação.
Parâmetros:
predicoes (list): Lista com as predições do modelo.
rotulos_reais (list): Lista com os rótulos verdadeiros.
Retorna:
float: Acurácia entre 0.0 e 1.0.
Exemplo:
>>> calcular_acuracia([1, 0, 1, 1], [1, 0, 0, 1])
0.75
"""
if len(predicoes) != len(rotulos_reais):
raise ValueError("As listas devem ter o mesmo tamanho.")
corretos = sum(p == r for p, r in zip(predicoes, rotulos_reais))
return corretos / len(rotulos_reais)
help(calcular_acuracia)
print(calcular_acuracia([1, 0, 1, 1], [1, 0, 0, 1]))
Saída:
0.75
Há uma última capacidade das funções que merece atenção: elas podem chamar a si mesmas. Recursão é a ferramenta natural para problemas de estrutura recursiva — árvores, dados hierárquicos, algoritmos como busca binária e merge sort. Toda função recursiva precisa de duas peças: o caso base, que é a condição de parada e sem o qual você tem recursão infinita, e o caso recursivo, que chama a própria função sobre um problema menor.
def contagem_regressiva(n):
if n <= 0: # caso base
print("Lançar!")
else: # caso recursivo
print(n)
contagem_regressiva(n - 1)
contagem_regressiva(5)
Saída:
5
4
3
2
1
Lançar!
O fatorial é o exemplo canônico, porque sua própria definição matemática já é recursiva:
def fatorial(n):
"""Calcula n! recursivamente."""
if n == 0 or n == 1: # caso base
return 1
return n * fatorial(n - 1) # caso recursivo
print(fatorial(5)) # 120
print(fatorial(10)) # 3628800
Acompanhar a cadeia de chamadas ajuda a enxergar o que acontece na memória:
fatorial(5)
= 5 * fatorial(4)
= 5 * 4 * fatorial(3)
= 5 * 4 * 3 * fatorial(2)
= 5 * 4 * 3 * 2 * fatorial(1)
= 5 * 4 * 3 * 2 * 1
= 120
Fibonacci segue o mesmo princípio, com dois casos base em vez de um:
def fibonacci(n):
"""Retorna o n-ésimo número de Fibonacci."""
if n <= 1: # casos base: fib(0) = 0, fib(1) = 1
return n
return fibonacci(n - 1) + fibonacci(n - 2)
for i in range(10):
print(f"fib({i}) = {fibonacci(i)}")
Saída:
fib(0) = 0
fib(1) = 1
fib(2) = 1
fib(3) = 2
fib(4) = 3
fib(5) = 5
fib(6) = 8
fib(7) = 13
fib(8) = 21
fib(9) = 34
A elegância cobra seu preço, no entanto. O Python limita a profundidade da recursão a mil chamadas por padrão, e a versão ingênua do Fibonacci recalcula os mesmos valores incontáveis vezes. Para problemas grandes, o caminho é a solução iterativa ou a memoização, que guarda os resultados já computados:
import sys
print(sys.getrecursionlimit()) # 1000
# Fibonacci com memoização (muito mais eficiente)
def fibonacci_memo(n, memo={}):
if n in memo:
return memo[n]
if n <= 1:
return n
memo[n] = fibonacci_memo(n - 1, memo) + fibonacci_memo(n - 2, memo)
return memo[n]
print(fibonacci_memo(50)) # 12586269025 — rápido!
Fechando o repertório, o Python moderno encoraja anotar os tipos esperados de parâmetros e retornos. As type hints não são obrigatórias nem verificadas em tempo de execução, mas tornam a assinatura autoexplicativa e habilitam análise estática com ferramentas como o mypy:
def calcular_media(valores: list[float]) -> float:
"""Calcula a média de uma lista de números."""
return sum(valores) / len(valores)
def normalizar(valor: float, minimo: float, maximo: float) -> float:
"""Normaliza um valor entre 0 e 1."""
return (valor - minimo) / (maximo - minimo)
def classificar(probabilidade: float, limiar: float = 0.5) -> str:
"""Classifica uma probabilidade em positivo ou negativo."""
return "positivo" if probabilidade >= limiar else "negativo"
Para ver tudo isso operando junto, vale montar um mini pipeline de pré-processamento — com funções pequenas, de responsabilidade única, documentadas e anotadas, orquestradas por uma função de nível mais alto. É bem próximo do que você encontra em projetos reais:
def carregar_dados() -> list[dict]:
"""Simula o carregamento de um dataset."""
return [
{"id": 1, "texto": " O produto é ótimo! ", "nota": 5},
{"id": 2, "texto": "Péssimo, não recomendo.", "nota": 1},
{"id": 3, "texto": None, "nota": 3},
{"id": 4, "texto": "Bom custo benefício", "nota": 4},
{"id": 5, "texto": " ", "nota": 2},
]
def limpar_texto(texto: str | None) -> str | None:
"""Remove espaços extras e retorna None para textos vazios."""
if texto is None:
return None
texto = texto.strip()
return texto if texto else None
def filtrar_validos(dados: list[dict]) -> list[dict]:
"""Remove registros com texto ausente ou vazio."""
return [d for d in dados if limpar_texto(d["texto"]) is not None]
def normalizar_notas(dados: list[dict], minimo: float = 1, maximo: float = 5) -> list[dict]:
"""Normaliza as notas para o intervalo [0, 1]."""
for registro in dados:
registro["nota_normalizada"] = (registro["nota"] - minimo) / (maximo - minimo)
return dados
def preprocessar(dados: list[dict]) -> list[dict]:
"""Executa o pipeline completo de pré-processamento."""
dados = filtrar_validos(dados)
for d in dados:
d["texto"] = limpar_texto(d["texto"])
dados = normalizar_notas(dados)
return dados
# Executando o pipeline
dados_brutos = carregar_dados()
dados_processados = preprocessar(dados_brutos)
print(f"Registros originais: {len(dados_brutos)}")
print(f"Registros válidos: {len(dados_processados)}\n")
for registro in dados_processados:
print(f"ID {registro['id']}: '{registro['texto']}'")
print(f" Nota: {registro['nota']} → Normalizada: {registro['nota_normalizada']:.2f}")
Saída:
Registros originais: 5
Registros válidos: 3
ID 1: 'O produto é ótimo!'
Nota: 5 → Normalizada: 1.00
ID 2: 'Péssimo, não recomendo.'
Nota: 1 → Normalizada: 0.00
ID 4: 'Bom custo benefício'
Nota: 4 → Normalizada: 0.75
Em resumo: funções nascem de def nome(parametros): e devolvem valores com return, inclusive vários de uma vez na forma de tupla. Os parâmetros podem ser posicionais, ter valor padrão, recolher extras com *args na forma de tupla ou com **kwargs na forma de dicionário. O escopo obedece à regra LEGB, com global e nonlocal liberando escrita nos níveis externos. Funções são objetos de primeira classe e circulam como argumento; lambdas resolvem os casos de uma linha; docstrings documentam e alimentam o help(); recursão exige caso base e atenção ao limite de profundidade; e type hints deixam a assinatura legível para pessoas e ferramentas.
Os seis exercícios abaixo passam por parâmetros, escopo, lambdas e recursão. Tente resolver cada um antes de abrir a resposta comentada.
-
Escreva uma função chamada
celsius_para_fahrenheitque receba uma temperatura em Celsius e retorne o equivalente em Fahrenheit. A fórmula é:F = (C × 9/5) + 32. Em seguida, usemap()com uma lambda para converter a lista abaixo inteira de uma vez.temperaturas_celsius = [0, 20, 37, 100, -10]✓ Resposta:def celsius_para_fahrenheit(celsius: float) -> float: """Converte temperatura de Celsius para Fahrenheit.""" return (celsius * 9/5) + 32 temperaturas_celsius = [0, 20, 37, 100, -10] temperaturas_fahrenheit = list(map(lambda c: celsius_para_fahrenheit(c), temperaturas_celsius)) print(temperaturas_fahrenheit)Saída:
[32.0, 68.0, 98.6, 212.0, 14.0]Ou de forma mais concisa, passando a função diretamente sem lambda:
temperaturas_fahrenheit = list(map(celsius_para_fahrenheit, temperaturas_celsius)) -
Escreva uma função
resumo_listaque receba qualquer quantidade de números via*argse retorne um dicionário com as chaves"quantidade","soma","media","minimo"e"maximo". Se nenhum número for passado, retorneNone.✓ Resposta:def resumo_lista(*args): """Retorna estatísticas básicas de uma lista de números.""" if not args: return None return { "quantidade": len(args), "soma": sum(args), "media": sum(args) / len(args), "minimo": min(args), "maximo": max(args) } print(resumo_lista(3, 7, 2, 9, 4)) print(resumo_lista())Saída:
{'quantidade': 5, 'soma': 25, 'media': 5.0, 'minimo': 2, 'maximo': 9} None -
Explique com suas palavras o que é escopo em Python e o que acontece no código abaixo. O código funciona? Por quê?
x = 10 def dobrar(): return x * 2 def incrementar(): x += 1 return x print(dobrar()) print(incrementar())✓ Resposta:O código não funciona completamente.
dobrar()funciona porque apenas lê a variável globalx, o que é permitido. Masincrementar()falha comUnboundLocalErrorporque tenta modificarxcomx += 1, que é equivalente ax = x + 1. Ao fazer isso, Python entende quexé uma variável local da função, mas então tenta lerxantes de atribuí-la — o que causa o erro.Para corrigir
incrementar(), precisaria declararglobal x:x = 10 def dobrar(): return x * 2 def incrementar(): global x x += 1 return x print(dobrar()) # 20 print(incrementar()) # 11 -
Escreva uma função recursiva
soma_digitos(n)que receba um inteiro positivo e retorne a soma de seus dígitos. Por exemplo,soma_digitos(1234)deve retornar10(1+2+3+4).✓ Resposta:def soma_digitos(n: int) -> int: """Retorna a soma dos dígitos de um inteiro positivo.""" if n < 10: # caso base: número de um dígito return n return (n % 10) + soma_digitos(n // 10) # último dígito + resto print(soma_digitos(1234)) # 10 print(soma_digitos(9999)) # 36 print(soma_digitos(7)) # 7A lógica:
n % 10pega o último dígito (1234 % 10 = 4),n // 10remove o último dígito (1234 // 10 = 123). A recursão continua até restar um único dígito. -
Reescreva a função abaixo de três formas: usando
defnormal, usandolambda, e usando list comprehension comfilter.# Original def apenas_positivos(numeros): resultado = [] for n in numeros: if n > 0: resultado.append(n) return resultado✓ Resposta:numeros = [-3, 5, -1, 8, 0, 2, -7, 4] # Versão 1: def normal (original, já dada) def apenas_positivos_v1(numeros): resultado = [] for n in numeros: if n > 0: resultado.append(n) return resultado # Versão 2: lambda com filter apenas_positivos_v2 = lambda nums: list(filter(lambda n: n > 0, nums)) # Versão 3: list comprehension def apenas_positivos_v3(numeros): return [n for n in numeros if n > 0] print(apenas_positivos_v1(numeros)) # [5, 8, 2, 4] print(apenas_positivos_v2(numeros)) # [5, 8, 2, 4] print(apenas_positivos_v3(numeros)) # [5, 8, 2, 4]A versão com list comprehension é considerada a mais Pythonica das três.
-
Explique a diferença entre
*argse**kwargs. Quando você usaria cada um? Dê um exemplo de situação em projetos de ML onde cada um seria útil.✓ Resposta:*argscaptura qualquer número de argumentos posicionais como uma tupla. Use quando você quer que a função aceite quantidades variáveis de valores sem nome.**kwargscaptura qualquer número de argumentos nomeados como um dicionário. Use quando você quer que a função aceite configurações ou opções identificadas por nome.Exemplo em ML com
*args: uma função que calcula a média de múltiplos scores de validação cruzada:def media_scores(*scores): return sum(scores) / len(scores) print(media_scores(0.82, 0.85, 0.79, 0.88)) # 0.835Exemplo em ML com
**kwargs: uma função que cria e registra hiperparâmetros de um experimento:def registrar_experimento(nome_modelo, **hiperparametros): print(f"Modelo: {nome_modelo}") for param, valor in hiperparametros.items(): print(f" {param}: {valor}") registrar_experimento( "RandomForest", n_estimators=100, max_depth=5, min_samples_split=2, random_state=42 )
Para aprofundar:
- Documentação oficial do Python — Definindo funções: https://docs.python.org/3/tutorial/controlflow.html#defining-functions
- Documentação oficial do Python — Escopos e namespaces: https://docs.python.org/3/tutorial/classes.html#python-scopes-and-namespaces
- PEP 3107 — Function Annotations: https://peps.python.org/pep-3107/
- PEP 484 — Type Hints: https://peps.python.org/pep-0484/
- Real Python — Defining Your Own Python Function: https://realpython.com/defining-your-own-python-function/
- Real Python — Python args and kwargs: https://realpython.com/python-kwargs-and-args/
- Real Python — Recursion in Python: https://realpython.com/python-recursion/
- Real Python — Lambda Functions in Python: https://realpython.com/python-lambda/