Funções são o principal mecanismo de organização de código em Python. Elas permitem nomear um bloco de instruções, reutilizá-lo quantas vezes for necessário e dividir problemas complexos em partes menores e gerenciáveis. Um programa bem estruturado é essencialmente uma coleção de funções bem definidas.
Definindo uma Função
A palavra-chave def inicia a definição de uma função:
def saudar():
print("Olá! Bem-vindo ao Python.")
# Chamando a função
saudar() # Olá! Bem-vindo ao Python.
saudar() # pode ser chamada quantas vezes quiser
A função só é executada quando chamada — a definição apenas a registra na memória.
Parâmetros e Argumentos
Funções se tornam mais úteis quando aceitam dados externos:
def saudar(nome):
print(f"Olá, {nome}! Bem-vindo ao Python.")
saudar("Ricardo") # Olá, Ricardo! Bem-vindo ao Python.
saudar("Ana") # Olá, Ana! Bem-vindo ao Python.
Parâmetro é o nome na definição da função (nome). Argumento é o valor passado na chamada ("Ricardo").
Funções podem ter múltiplos parâmetros:
def apresentar(nome, idade, cidade):
print(f"{nome}, {idade} anos, mora em {cidade}.")
apresentar("Carlos", 28, "São Paulo")
Parâmetros com Valor Padrão
É possível definir valores padrão para parâmetros — tornando-os opcionais na chamada:
def conectar(host, porta=5432, ssl=True):
print(f"Conectando em {host}:{porta} (SSL: {ssl})")
conectar("db.servidor.com") # usa porta 5432 e ssl True
conectar("db.servidor.com", 3306) # porta diferente
conectar("db.servidor.com", 3306, False) # sem SSL
Parâmetros com valor padrão devem sempre vir depois dos parâmetros obrigatórios.
E aqui mora a armadilha mais conhecida da linguagem: nunca use uma lista, um dicionário ou um conjunto como valor padrão. O padrão é avaliado uma única vez, quando a função é definida — e não a cada chamada. Se ele for mutável, todas as chamadas compartilham o mesmo objeto, e o que uma modifica a seguinte encontra modificado.
def adicionar(item, lista=[]): # ERRADO
lista.append(item)
return lista
print(adicionar("a")) # ['a']
print(adicionar("b")) # ['a', 'b'] — e nao ['b']
O remédio é sempre o mesmo: use None como padrão e crie o objeto dentro da função.
def adicionar(item, lista=None): # CERTO
if lista is None:
lista = []
lista.append(item)
return lista
A mesma regra vale para qualquer padrão que seja calculado: escrever def registrar(t=datetime.now()) congela o instante em que o arquivo foi carregado, e todas as chamadas recebem aquele mesmo horário.
Argumentos Nomeados (Keyword Arguments)
Ao chamar uma função, você pode nomear os argumentos — tornando a ordem irrelevante e o código mais legível:
def criar_usuario(nome, email, admin=False):
print(f"Usuário: {nome} | E-mail: {email} | Admin: {admin}")
criar_usuario(email="ana@exemplo.com", nome="Ana", admin=True)
Retornando Valores
Funções podem devolver resultados com return:
def calcular_imc(peso, altura):
imc = peso / altura ** 2
return imc
resultado = calcular_imc(70, 1.75)
print(f"IMC: {resultado:.2f}") # IMC: 22.86
Quando return é executado, a função encerra imediatamente. É possível retornar múltiplos valores — Python os empacota automaticamente em uma tupla:
def minmax(numeros):
return min(numeros), max(numeros)
menor, maior = minmax([4, 1, 9, 3, 7])
print(menor, maior) # 1 9
Uma função sem return explícito retorna None implicitamente.
Escopo de Variáveis
Escopo define onde uma variável é visível e acessível, e vale relembrar o que foi visto em Laços de Repetição: for e while: em Python, if, for e while não criam escopo. Quem cria são a função, a classe e o módulo — e é por isso que a variável de um laço sobrevive a ele, enquanto a de uma função não escapa dela. A regra de busca chama-se LEGB:
- Local — dentro da função atual
- Enclosing — em funções externas que envolvem a atual
- Global — no nível do módulo
- Built-in — nomes nativos do Python (
print,len, etc.)
x = 10 # variável global
def mostrar():
x = 20 # variável local — não afeta a global
print(x)
mostrar() # 20
print(x) # 10
Para modificar uma variável global dentro de uma função, use global — mas use com cautela, pois dificulta a leitura e o teste do código:
contador = 0
def incrementar():
global contador
contador += 1
incrementar()
incrementar()
print(contador) # 2
Funções como Objetos
Em Python, funções são objetos de primeira classe — podem ser atribuídas a variáveis, passadas como argumentos e retornadas por outras funções:
def dobrar(x):
return x * 2
def triplicar(x):
return x * 3
def aplicar(func, valor):
return func(valor)
print(aplicar(dobrar, 5)) # 10
print(aplicar(triplicar, 5)) # 15
Funções Lambda
Para funções simples de uma linha, Python oferece as lambdas — funções anônimas:
quadrado = lambda x: x ** 2
print(quadrado(4)) # 16
# Muito usadas com funções como sorted(), map(), filter()
nomes = ["Carlos", "Ana", "Bruno", "Diana"]
ordenados = sorted(nomes, key=lambda nome: len(nome))
print(ordenados) # ['Ana', 'Bruno', 'Diana', 'Carlos']
Lambdas são restritas a uma única expressão. Para lógica mais complexa, defina uma função normal com def.
Uma ressalva sobre o primeiro exemplo: atribuir uma lambda a um nome, como em quadrado = lambda x: x ** 2, é desaconselhado pela PEP 8, e ferramentas de análise acusam isso. O motivo é prático: a lambda nasce sem nome próprio, então o rastreamento de um erro mostra <lambda> em vez de quadrado, e ela não aceita docstring. Se a função vai ter nome, use def — a lambda existe para o caso em que ela é passada direto como argumento, como no key= do sorted logo abaixo.
Docstrings
Boas funções documentam seu propósito com uma docstring — uma string logo após o def:
def calcular_juros(principal, taxa, periodo):
"""
Calcula o juros simples sobre um valor.
Args:
principal: Valor inicial aplicado.
taxa: Taxa de juros por período (em decimal, ex: 0.05 para 5%).
periodo: Número de períodos.
Returns:
O valor total acumulado após os juros.
"""
return principal * (1 + taxa * periodo)
help(calcular_juros) # exibe a docstring formatada
Exemplo Completo: Calculadora
def somar(a, b):
"""Retorna a soma de dois números."""
return a + b
def subtrair(a, b):
"""Retorna a subtração de dois números."""
return a - b
def multiplicar(a, b):
"""Retorna o produto de dois números."""
return a * b
def dividir(a, b):
"""Retorna a divisão de dois números. Lança erro se b for zero."""
if b == 0:
raise ValueError("Divisão por zero não é permitida.")
return a / b
def calcular(a, operacao, b):
operacoes = {
"+": somar,
"-": subtrair,
"*": multiplicar,
"/": dividir,
}
if operacao not in operacoes:
raise ValueError(f"Operação '{operacao}' não suportada.")
return operacoes[operacao](a, b)
print(calcular(10, "+", 5)) # 15
print(calcular(10, "/", 4)) # 2.5
print(calcular(3, "*", 7)) # 21
Há uma frase desta parte que carrega mais do que aparenta: em Python, funções são objetos de primeira classe. Isso não é curiosidade acadêmica — é o que permite passar uma função como argumento para sorted, guardar uma numa estrutura de dados, devolver uma de dentro de outra. Todo o resto que a linguagem oferece daí para a frente, dos decoradores às funções de ordem superior, se apoia nesse fato simples: def não é uma declaração especial, é uma atribuição que amarra um objeto a um nome.
E há um ponto que separa quem escreve Python de quem escreve outra linguagem com sintaxe de Python: o valor padrão é avaliado quando a função é definida, não quando é chamada. Enquanto o padrão for um número, um texto ou None, isso não tem consequência visível. No dia em que for uma lista, um dicionário ou o resultado de uma chamada, o comportamento muda sem aviso e o defeito aparece longe dali. É a razão de a forma com None ser tão repetida — não é preferência de estilo, é a única que funciona.
Fontes e leituras recomendadas
- Funções — documentação oficial — https://docs.python.org/3/tutorial/controlflow.html#defining-functions
- Escopo e namespaces — https://docs.python.org/3/tutorial/classes.html#python-scopes-and-namespaces
- Expressões lambda — https://docs.python.org/3/reference/expressions.html#lambda
- Convenções de docstring (PEP 257) — https://peps.python.org/pep-0257/
- MATTHES, Eric. Python Crash Course. 3. ed. No Starch Press, 2023. Cap. 8.
- LUTZ, Mark. Learning Python. 5. ed. O'Reilly Media, 2013. Cap. 16–17.
- RAMALHO, Luciano. Fluent Python. 2. ed. O'Reilly Media, 2022. Cap. 7.
Exercícios
Exercício 1
Uma função de carrinho é escrita como def adicionar(item, carrinho=[]):. Em desenvolvimento funciona. Em produção, clientes começam a ver produtos que nunca escolheram aparecendo no carrinho. Explique o mecanismo e o conserto.
Ver resposta
✓ Resposta: O valor padrão é avaliado uma única vez, no momento em que o def é executado — ou seja, quando o módulo é importado — e não a cada chamada. Existe, portanto, uma lista, guardada no próprio objeto função (dá para vê-la em adicionar.__defaults__) e compartilhada por todas as chamadas que não passarem um carrinho. A primeira chamada devolve ['a'], a segunda devolve ['a', 'b'], e assim por diante, acumulando para sempre. Em produção isso vira o pior tipo de defeito: os dados de um cliente vazam para o próximo, o acúmulo cresce enquanto o processo estiver de pé, e nada disso reproduz em desenvolvimento, onde o servidor reinicia a cada alteração e a lista nasce limpa o tempo todo. O conserto é usar None como sentinela e criar o objeto dentro da função: def adicionar(item, carrinho=None):, seguido de if carrinho is None: carrinho = []. A regra vale para tudo que é mutável — lista, dicionário, conjunto, e qualquer instância de classe própria. Vale também para padrão calculado: def registrar(quando=datetime.now()) congela o horário da importação, e todas as chamadas recebem o mesmo instante. E há um detalhe que transforma a regra em algo útil: esse mesmo comportamento é aproveitado de propósito para criar um objeto sentinela exclusivo, quando None é um valor legítimo do domínio.
Exercício 2
O artigo apresenta global para alterar uma variável de módulo dentro de uma função, com a ressalva de usar com cautela. Descreva concretamente o que se perde ao usar global, e qual é a alternativa.
Ver resposta
✓ Resposta: Perde-se a possibilidade de raciocinar sobre a função olhando só para ela. Uma função que recebe tudo por parâmetro e devolve tudo por return tem um contrato visível na assinatura: dadas as mesmas entradas, o mesmo resultado. Com global, o resultado passa a depender de um estado que está em outro lugar do arquivo e que qualquer outra função pode ter alterado antes. Três consequências práticas. Testar fica caro: cada teste precisa montar e desmontar o estado global, e a ordem de execução dos testes passa a importar — o sintoma clássico é a suíte que passa inteira e falha quando se roda um teste sozinho. Depurar fica caro: para descobrir por que o valor está errado, é preciso procurar todos os pontos que escrevem naquele nome, em vez de olhar a chamada. E concorrência quebra: duas threads executando a função mexem na mesma variável, e contador += 1 não é atômico — é leitura, soma e escrita, com espaço para uma perder a atualização da outra. A alternativa depende do caso: para contar alguma coisa, devolva o valor e deixe quem chamou acumular; para estado que precisa mesmo persistir entre chamadas, use uma classe, onde o estado fica explícito no objeto e cada instância tem o seu; para configuração lida por todo o programa, leia uma vez e passe adiante. Vale notar que global só é necessário para reatribuir o nome — ler uma variável de módulo, ou chamar um método que a modifica no lugar, funciona sem declaração nenhuma, o que é fonte frequente de confusão.
Exercício 3
O artigo mostra return min(numeros), max(numeros) e diz que Python empacota os valores numa tupla. Que cuidados esse recurso exige quando a função cresce, e o que usar no lugar?
Ver resposta
✓ Resposta: O recurso é ótimo com dois valores e vira armadilha a partir de três, porque quem chama passa a depender da ordem, e a ordem não está escrita em lugar nenhum. Com menor, maior = minmax(...) ninguém erra; com nome, email, idade, cidade = buscar(...), trocar dois campos de lugar na definição produz um bug que o interpretador não pega — os tipos batem, a atribuição funciona, e o e-mail vai para o campo do nome. Há ainda o problema da evolução: acrescentar um valor ao retorno quebra todas as chamadas existentes de uma vez, porque o desempacotamento exige o número exato. As alternativas, em ordem de peso: NamedTuple, do módulo typing, que mantém o desempacotamento funcionando e dá nome aos campos, permitindo resultado.email; dataclass, quando o retorno é um objeto do domínio com comportamento próprio; e um dicionário, quando as chaves são realmente dinâmicas — nunca quando são fixas e conhecidas, caso em que ele só troca um erro de ordem por um erro de digitação em texto. Um detalhe que ajuda a enxergar o que está acontecendo: o return a, b não tem nada de especial: a vírgula é que cria a tupla, e os parênteses do outro lado são opcionais. É o mesmo mecanismo de a, b = b, a, a troca de valores sem variável auxiliar.
Exercício 4
Você precisa ordenar uma lista de dicionários de produtos por preço decrescente e, em caso de empate, por nome em ordem alfabética. Escreva a chave de ordenação e explique por que essa solução funciona.
Ver resposta
✓ Resposta: A chave é sorted(produtos, key=lambda p: (-p["preco"], p["nome"])). Funciona por duas propriedades combinadas. A primeira é que tuplas se comparam elemento a elemento: Python olha o primeiro item, e só desempata com o segundo se os primeiros forem iguais — exatamente a regra de "ordene por preço e, se empatar, por nome". A segunda é que o key= aceita qualquer função que devolva algo comparável, tupla incluída. O sinal de menos resolve a inversão de um critério só: não dá para usar reverse=True aqui, porque ele inverteria os dois, e o nome ficaria em ordem decrescente também. Esse truque, porém, só funciona com número; para inverter um critério de texto, o caminho é ordenar duas vezes, do critério menos importante para o mais importante, aproveitando que o sorted do Python é estável — ele preserva a ordem relativa dos elementos que a chave considera iguais, e é essa garantia que faz a ordenação em etapas dar o resultado certo. Duas observações práticas: para acessar atributos em vez de chaves, operator.itemgetter("preco", "nome") e operator.attrgetter são mais rápidos e mais legíveis que a lambda; e sorted devolve uma lista nova, enquanto lista.sort() ordena no lugar e devolve None — atribuir o resultado de .sort() a uma variável é um erro comum que deixa a variável valendo None.
Exercício 5
Uma função definida dentro de outra tenta alterar uma variável da função externa e não consegue. O global não resolve. O que está acontecendo?
Ver resposta
✓ Resposta: É o E do LEGB — o escopo enclosing, o da função que envolve. A regra de Python é que atribuir a um nome dentro de uma função torna esse nome local a ela, mesmo que exista um igual lá fora; a partir daí, ler o nome antes de atribuí-lo levanta UnboundLocalError, que é a mensagem confusa que aparece nesse caso. O global não resolve porque ele aponta para o escopo do módulo, e a variável está na função de fora, que não é o módulo — usá-lo aqui cria ou altera uma variável global e deixa a de dentro intocada. A palavra certa é nonlocal, que diz "esse nome pertence ao escopo da função que me envolve, e é nele que quero escrever". Vale entender por que a distinção existe: sem ela, qualquer atribuição dentro de uma função aninhada teria de sair procurando o nome escopo acima, e seria impossível saber, olhando o código, se uma linha cria uma variável nova ou modifica uma de fora. Python prefere obrigar a declarar a intenção. Na prática, nonlocal aparece pouco e quase sempre em closures que guardam estado — um contador, um acumulador, um cache —, e em boa parte desses casos uma classe deixa o código mais claro. E cuidado com o caso que engana: se a variável de fora for uma lista ou um dicionário e você só chamar append ou atribuir a uma chave, nada disso é necessário, porque não há reatribuição do nome — você está modificando o objeto, não trocando a quem o nome aponta.