Funções: definição, parâmetros e escopo

Funções: definição, parâmetros e escopo

Nomear um bloco de código é o primeiro passo para escrever programas que crescem sem virar um emaranhado. Definição, parâmetros e argumentos, o valor padrão e a armadilha que ele esconde quando é mutável, o retorno em tupla, a regra LEGB de escopo, funções como objetos de primeira classe e o lugar certo da lambda.
Python

• • 15 min de leitura

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

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.

Comentários

Mais em Python

Trabalhando com Datas e Horas
Trabalhando com Datas e Horas

Datas e horas em Python com datetime, timedelta, zoneinfo e dateutil. Com as…

WebSockets e Comunicação em Tempo Real em Python
WebSockets e Comunicação em Tempo Real em Python

WebSockets e SSE com FastAPI, do eco ao chat e às notificações, com as falhas…

Decoradores e Metaprogramação
Decoradores e Metaprogramação

Decoradores, closures e metaprogramação em Python, do açúcar sintático ao que…