6  Mais Funções

Neste capítulo, vamos aprofundar o estudo de funções depois da base de Funções e Estruturas de Dados.

Ordem deste capítulo: 1. Boas práticas para iniciantes 2. Recursão 3. Assinaturas avançadas 4. Funções lambda 5. Funções não puras


6.1 Boas práticas para iniciantes

Boas práticas tornam o código mais legível e mais fácil de manter.

Exemplos Práticos:

Ponto de entrada principal (padroes_entrada_principal.py): Crie um programa que leia dois números decimais e exiba a soma deles, organizado em funções pequenas: somar, que retorna a soma, montar_mensagem, que retorna a frase final, e main, que coordena entrada, processamento e saída. A função main só deve ser executada dentro do bloco if __name__ == "__main__":.

def somar(a, b):
    return a + b
def montar_mensagem(resultado):
    return f"Resultado da soma: {resultado}"
def main():
    numero1 = float(input("Digite o primeiro número: "))
    numero2 = float(input("Digite o segundo número: "))
    total = somar(numero1, numero2)
    mensagem = montar_mensagem(total)
    print(mensagem)
if __name__ == "__main__":
    main()

Exemplo de execução:

Digite o primeiro número: 2.5
Digite o segundo número: 4
Resultado da soma: 6.5

Cada função tem uma única tarefa e um nome que a descreve: somar faz a conta e montar_mensagem prepara o texto. A main organiza o programa na ordem natural, ou seja, lê os números, processa os dados e exibe o resultado. Quem abre o arquivo pode começar a leitura pela main e ter uma visão geral antes de entrar nos detalhes de cada função.

As duas últimas linhas formam o ponto de entrada do programa. O Python cria automaticamente a variável especial __name__, que vale "__main__" quando o arquivo é executado diretamente; nesse caso, a main é chamada. Quando o arquivo é importado por outro programa, por exemplo para reaproveitar a função somar, a main não executa sozinha.

Calculador de frete (calculador_frete.py): Crie um programa que leia o peso do pacote em kg e a distância da entrega em km (decimais) e o tipo de envio (“normal” ou “expresso”) e exiba o frete total. Divida o cálculo em funções.

  1. Custo base pelo peso: R$ 12,00 até 1 kg, R$ 20,00 acima de 1 kg e até 5 kg e R$ 35,00 acima de 5 kg.
  2. Adicional de distância: R$ 0,05 por km.
  3. Fator do tipo de envio: 1.0 para “normal” e 1.5 para “expresso”.
  4. Uma função de alto nível combina as anteriores: soma o custo base e o adicional e aplica o fator ao subtotal.
def calcular_custo_base_por_peso(peso):
    if peso <= 1:
        return 12.0
    if peso <= 5:
        return 20.0
    return 35.0
def calcular_adicional_distancia(distancia):
    return distancia * 0.05
def aplicar_fator_envio(valor, tipo_envio):
    if tipo_envio == "expresso":
        return valor * 1.5
    return valor
def calcular_frete_total(peso, distancia, tipo_envio):
    base = calcular_custo_base_por_peso(peso)
    adicional = calcular_adicional_distancia(distancia)
    subtotal = base + adicional
    return aplicar_fator_envio(subtotal, tipo_envio)
peso_pacote = float(input("Digite o peso do pacote (kg): "))
distancia_entrega = float(input("Digite a distância (km): "))
tipo_envio = input("Digite o tipo de envio (normal/expresso): ")
total = calcular_frete_total(peso_pacote, distancia_entrega, tipo_envio)
print(f"Frete total: R$ {total}")

Exemplo de execução:

Digite o peso do pacote (kg): 3
Digite a distância (km): 100
Digite o tipo de envio (normal/expresso): expresso
Frete total: R$ 37.5

Cada regra do frete fica em uma função própria. Nas funções de peso e de tipo de envio, cada return encerra a função assim que a faixa certa é encontrada, o que dispensa o elif e o else. A função calcular_frete_total descreve o cálculo completo em quatro passos: obtém o custo base, obtém o adicional, soma os dois e aplica o fator de envio.

O programa principal só precisa ler os dados e chamar a função de alto nível. Com 3 kg, 100 km e envio expresso, o frete é (20.0 + 5.0) × 1.5 = 37.5.

Refatoração com main (refactor_fluxo_principal.py): Crie um programa que leia o nome e a nota (decimal) de um aluno e exiba uma mensagem com o nome, a nota e a situação: “Aprovado” para nota a partir de 7, “Recuperação” para nota a partir de 5 e “Reprovado” abaixo disso. Use uma função para ler os dados, outra para classificar a nota, outra para montar a mensagem e uma função main, executada com if __name__ == "__main__":.

def ler_dados():
    nome = input("Nome do aluno: ")
    nota = float(input("Nota do aluno: "))
    return nome, nota
def classificar_nota(nota):
    if nota >= 7:
        return "Aprovado"
    if nota >= 5:
        return "Recuperação"
    return "Reprovado"
def montar_mensagem(nome, nota, situacao):
    return f"Aluno: {nome} | Nota: {nota} | Situação: {situacao}"
def main():
    nome, nota = ler_dados()
    situacao = classificar_nota(nota)
    print(montar_mensagem(nome, nota, situacao))
if __name__ == "__main__":
    main()

Exemplo de execução:

Nome do aluno: Ana
Nota do aluno: 6.5
Aluno: Ana | Nota: 6.5 | Situação: Recuperação

A função ler_dados devolve dois valores de uma vez, separados por vírgula no return, e o Python agrupa esses valores em uma tupla. Na main, essa tupla é desempacotada em nome, nota. A função classificar_nota usa a mesma sequência de return do exemplo anterior para escolher a situação.

Com essa organização, a main resume o programa em três linhas, ler, classificar e exibir, e cada detalhe fica na função responsável por ele.


6.2 Recursão

Recursão acontece quando uma função chama a si mesma. Ela precisa de caso base para parar.

Exemplos Práticos:

Contagem recursiva (contagem_recursiva.py): Crie um programa que leia um número inteiro maior ou igual a zero e faça uma contagem regressiva até 0 com uma função recursiva contagem_regressiva. A função exibe o número atual e, se ele for maior que zero, chama a si mesma com o número menos 1.

def contagem_regressiva(numero):
    print(numero)
    if numero > 0:
        contagem_regressiva(numero - 1)
inicio = int(input("Digite um número inteiro não negativo: "))
contagem_regressiva(inicio)

Exemplo de execução:

Digite um número inteiro não negativo: 3
3
2
1
0

Uma função recursiva chama a si mesma. Aqui, cada chamada exibe o número recebido e, se ele for maior que zero, chama a função de novo com o número menos 1. Com a entrada 3, a função exibe 3 e chama contagem_regressiva(2), que exibe 2 e chama a próxima, até chegar a 0.

Quando o número é 0, a condição numero > 0 é falsa e nenhuma nova chamada acontece. Esse é o caso base, a situação em que a recursão para. Todo programa recursivo precisa de um caso base e cada chamada precisa ficar mais perto dele; sem isso, as chamadas continuariam até o Python interromper o programa com um erro.

Fatorial recursivo (fatorial_recursivo.py): Crie um programa que leia um número inteiro não negativo n e exiba o fatorial dele, calculado por uma função recursiva fatorial(n). O caso base é n igual a 0 ou 1, que retorna 1; nos demais casos, a função retorna n * fatorial(n - 1).

def fatorial(n):
    if n <= 1:
        return 1
    return n * fatorial(n - 1)
numero = int(input("Digite um número inteiro não negativo: "))
print(f"Fatorial de {numero}: {fatorial(numero)}")

Exemplo de execução:

Digite um número inteiro não negativo: 5
Fatorial de 5: 120

A função segue a definição matemática do fatorial. O caso base devolve 1 quando n é 0 ou 1, e o passo recursivo calcula n * fatorial(n - 1). Para obter fatorial(4), o Python precisa de fatorial(3), que precisa de fatorial(2), que precisa de fatorial(1). Essa última chamada cai no caso base e devolve 1. A partir daí, as chamadas pendentes terminam na ordem inversa: fatorial(2) devolve 2 × 1 = 2, fatorial(3) devolve 3 × 2 = 6 e fatorial(4) devolve 4 × 6 = 24.

Rastreando o fatorial (rastreador_fatorial_recursivo.py): Crie uma versão do fatorial recursivo que mostre a pilha de chamadas. A função fatorial_com_rastro(n) exibe “Entrando em fatorial(n)” ao começar e “Retornando para fatorial(n)” antes de retornar. O programa principal lê n e exibe o resultado final.

def fatorial_com_rastro(n):
    print(f"Entrando em fatorial({n})")
    if n <= 1:
        print(f"Retornando 1 para fatorial({n})")
        return 1
    resultado = n * fatorial_com_rastro(n - 1)
    print(f"Retornando {resultado} para fatorial({n})")
    return resultado
numero = int(input("Digite um inteiro não negativo: "))
print(f"Resultado final: {fatorial_com_rastro(numero)}")

Exemplo de execução:

Digite um inteiro não negativo: 3
Entrando em fatorial(3)
Entrando em fatorial(2)
Entrando em fatorial(1)
Retornando 1 para fatorial(1)
Retornando 2 para fatorial(2)
Retornando 6 para fatorial(3)
Resultado final: 6

A função calcula o fatorial como no exemplo anterior, mas exibe uma mensagem no início de cada chamada e outra antes de cada retorno. Para isso, o resultado da chamada recursiva fica guardado na variável resultado, o que permite exibir o valor antes de devolver. Na execução com a entrada 3, cada chamada começa a seguinte antes de terminar, e os retornos acontecem na ordem inversa, a partir do caso base. Essa estrutura, em que a última chamada a começar é a primeira a terminar, é chamada de pilha de chamadas.


6.3 Assinaturas avançadas

Assinaturas avançadas aumentam a flexibilidade das funções.

Exemplos Práticos:

Assinaturas avançadas (assinaturas_avancadas.py): Crie um programa, sem entrada do usuário, com três funções: somar_varios(*args), que soma qualquer quantidade de valores; montar_relatorio(**kwargs), que monta o relatório de um aluno a partir dos argumentos nomeados nome e nota; e potencia(base, expoente, /), cujos parâmetros só podem ser passados por posição. Exiba o resultado de uma chamada de cada função.

def somar_varios(*args):
    return sum(args)
def montar_relatorio(**kwargs):
    nome = kwargs.get("nome", "Sem nome")
    nota = kwargs.get("nota", 0)
    return f"Aluno: {nome} | Nota: {nota}"
def potencia(base, expoente, /):
    return base**expoente
print(somar_varios(2, 3, 5))
print(montar_relatorio(nome="Ana", nota=8.5))
print(potencia(2, 4))

Exemplo de execução:

10
Aluno: Ana | Nota: 8.5
16

O *args faz a função aceitar qualquer quantidade de argumentos posicionais, que chegam como uma tupla. Na chamada somar_varios(2, 3, 5), args vale (2, 3, 5), e a função sum() devolve 10. O **kwargs faz o mesmo com argumentos nomeados, no formato nome=valor, que chegam como um dicionário. Por isso a função usa o .get() com um valor padrão, para o caso de algum argumento não ser informado.

A barra / obriga os parâmetros escritos antes dela a serem passados por posição. A chamada potencia(2, 4) funciona e devolve 16, já que o operador ** calcula potências, mas potencia(base=2, expoente=4) causaria um erro.

Assinaturas simples (padroes_entrada_principal.py): Volte ao programa da seção de boas práticas e observe como assinaturas simples já tornam o código legível.

def somar(a, b):
    return a + b
def montar_mensagem(resultado):
    return f"Resultado da soma: {resultado}"
def main():
    numero1 = float(input("Digite o primeiro número: "))
    numero2 = float(input("Digite o segundo número: "))
    total = somar(numero1, numero2)
    mensagem = montar_mensagem(total)
    print(mensagem)
if __name__ == "__main__":
    main()

Exemplo de execução:

Digite o primeiro número: 2.5
Digite o segundo número: 4
Resultado da soma: 6.5

As assinaturas somar(a, b) e montar_mensagem(resultado) dizem, sem comentários, quantos dados cada função espera e para que eles servem. Os recursos avançados desta seção são úteis quando o número de argumentos varia. Quando ele é fixo, uma assinatura simples como essas é a escolha mais legível.

Validador de assinaturas (validador_assinaturas.py): Crie um programa, sem entrada do usuário, com as funções calcular_media(*notas), que retorna a média das notas ou 0 se nenhuma nota for passada; perfil_aluno(**dados), que monta uma mensagem com o nome e o curso do aluno; e potencia(base, expoente, /). Exiba o resultado de uma chamada de cada função.

def calcular_media(*notas):
    if not notas:
        return 0
    return sum(notas) / len(notas)
def perfil_aluno(**dados):
    nome = dados.get("nome", "Sem nome")
    curso = dados.get("curso", "Sem curso")
    return f"Aluno: {nome} | Curso: {curso}"
def potencia(base, expoente, /):
    return base**expoente
print(f"Média: {calcular_media(7, 8, 9)}")
print(perfil_aluno(nome="Lia", curso="Engenharia"))
print(f"Potência: {potencia(2, 3)}")

Exemplo de execução:

Média: 8.0
Aluno: Lia | Curso: Engenharia
Potência: 8

Em calcular_media, as notas chegam em uma tupla, e a condição not notas é verdadeira quando essa tupla está vazia, ou seja, quando a função é chamada sem argumentos. Nesse caso, o return 0 evita a divisão por zero. Na chamada com 7, 8 e 9, a média é 8.0. O nome depois dos asteriscos é livre: *notas e **dados funcionam como o *args e o **kwargs do exemplo anterior.


6.4 Funções lambda

A palavra-chave lambda cria funções anônimas (sem nome) em uma única expressão, usando a sintaxe lambda parâmetros: expressão. Diferente de def, ela não precisa de nome nem de return, pois o resultado da expressão é retornado automaticamente. É útil para casos pontuais em que definir uma função completa seria desnecessário, como em argumentos key de sorted(), min() e max().

Exemplos Práticos:

Funções anônimas (funcoes_anonimas.py): Crie um programa, sem entrada do usuário, que defina uma lambda para dobrar um número e use outra lambda como key do sorted para ordenar a lista [("Ana", 8.0), ("Bruno", 6.5), ("Carla", 9.2)] pela nota, da maior para a menor. Exiba os dois resultados.

dobro = lambda x: x * 2
pares = [("Ana", 8.0), ("Bruno", 6.5), ("Carla", 9.2)]
ordenados = sorted(pares, key=lambda item: item[1], reverse=True)
print(dobro(7))
print(ordenados)

Exemplo de execução:

14
[('Carla', 9.2), ('Ana', 8.0), ('Bruno', 6.5)]

A lambda cria uma função em uma única expressão: antes dos dois-pontos fica o parâmetro e, depois, o valor devolvido. A lambda x: x * 2 equivale a uma função definida com def que devolve x * 2, e dobro(7) resulta em 14.

No sorted(), o parâmetro key recebe uma função que diz qual valor comparar em cada elemento da lista. A lambda item: item[1] escolhe a posição 1 de cada tupla, que é a nota, e o reverse=True inverte a ordem, da maior nota para a menor. O resultado começa por ('Carla', 9.2).

Frequência de palavras (contador_frequencia_palavras.py): Volte ao contador de palavras do capítulo de estruturas de dados, resolvido sem lambda, e analise a legibilidade.

relatorio = input("Digite a observacao de inspecao: ").strip().lower()
palavras = relatorio.split()
frequencia = {}
for palavra in palavras:
    frequencia[palavra] = frequencia.get(palavra, 0) + 1
print(f"Frequencia de palavras: {frequencia}")

Exemplo de execução:

Digite a observacao de inspecao:   Falha no sensor falha no cabo  
Frequencia de palavras: {'falha': 2, 'no': 2, 'sensor': 1, 'cabo': 1}

Neste programa, o laço com get resolve o problema de forma direta, e uma lambda não deixaria o código mais claro. Prefira a lambda para expressões curtas passadas como argumento, como o key do sorted(). Quando a lógica tem mais de um passo, uma função com def ou um laço é mais fácil de ler.

Ordenação com lambda (ordenador_com_lambda.py): Crie um programa, sem entrada do usuário, que ordene a lista [("A", 3), ("B", 1), ("C", 2)] pelo segundo elemento de cada tupla, em ordem crescente e em ordem decrescente, e exiba a lista original e as duas listas ordenadas.

dados = [("A", 3), ("B", 1), ("C", 2)]
ordenado_crescente = sorted(dados, key=lambda item: item[1])
ordenado_decrescente = sorted(dados, key=lambda item: item[1], reverse=True)
print(f"Original: {dados}")
print(f"Crescente: {ordenado_crescente}")
print(f"Decrescente: {ordenado_decrescente}")

Exemplo de execução:

Original: [('A', 3), ('B', 1), ('C', 2)]
Crescente: [('B', 1), ('C', 2), ('A', 3)]
Decrescente: [('A', 3), ('C', 2), ('B', 1)]

As duas ordenações usam a mesma key=lambda item: item[1], que compara as tuplas pelo segundo elemento, e diferem apenas no reverse=True. O sorted() não altera a lista original: ele devolve uma lista nova, ordenada. Por isso dados continua na ordem em que foi criada, enquanto as outras duas variáveis guardam [('B', 1), ('C', 2), ('A', 3)] e [('A', 3), ('C', 2), ('B', 1)].


6.5 Funções não puras

No capítulo anterior sobre funções, priorizamos uma abordagem importante: sempre que possível, a função deve receber dados por parâmetros e retornar resultados com return. A entrada com input() e a saída com print() ficam no script principal. Funções que sempre retornam o mesmo resultado para os mesmos argumentos e não causam efeitos colaterais são conhecidas como funções puras.

Já funções não puras têm efeitos colaterais, como leitura de input(), uso de print() interno ou modificação de variáveis externas. Elas são práticas em alguns cenários, mas podem dificultar testes e reutilização.

Exemplos Práticos:

Pura e não pura (comparacao_puro_impuro.py): Crie um programa que some dois números inteiros de duas formas. A função pura soma_pura(a, b) recebe os números e retorna a soma; a leitura e a exibição ficam no programa principal. A função não pura soma_nao_pura() lê os números com input e exibe a soma com print dentro dela. Execute as duas versões e compare.

def soma_pura(a, b):
    return a + b
def soma_nao_pura():
    valor1 = int(input("(Não pura) Digite o primeiro número: "))
    valor2 = int(input("(Não pura) Digite o segundo número: "))
    print(f"(Não pura) Soma: {valor1 + valor2}")
numero1 = int(input("Digite o primeiro número: "))
numero2 = int(input("Digite o segundo número: "))
resultado = soma_pura(numero1, numero2)
print(f"(Pura) Soma: {resultado}")
soma_nao_pura()

Exemplo de execução:

Digite o primeiro número: 2
Digite o segundo número: 3
(Pura) Soma: 5
(Não pura) Digite o primeiro número: 4
(Não pura) Digite o segundo número: 5
(Não pura) Soma: 9

A soma_pura recebe os dados por parâmetros e devolve o resultado com return, deixando a leitura e a exibição para o programa principal. Ela sempre devolve o mesmo resultado para os mesmos argumentos, e o valor devolvido pode ser reaproveitado em outros cálculos, como em soma_pura(2, 3) * 10.

A soma_nao_pura lê os dados com input() e exibe o resultado com print() por conta própria, sem parâmetros nem return. Como depende do teclado e da tela, ela só serve para aquela interação específica e é mais difícil de testar.

Eco (eco.py): Este é o primeiro exemplo de input() do livro, que pergunta o nome do usuário e o cumprimenta.

nome_usuario = input("Qual é o seu nome? ")
print(f"Bem-vindo ao sistema, {nome_usuario}.")

Exemplo de execução:

Qual é o seu nome? Maria
Bem-vindo ao sistema, Maria.

Aqui, a interação com o usuário é o próprio objetivo do programa: ele lê o nome e exibe a saudação. Nesse tipo de situação, ler e exibir os dados diretamente é adequado, e separar o código em uma função pura não traria ganho.

Decisor de pureza (decisor_pureza_funcoes.py): Crie um programa, sem entrada do usuário, que apresente três cenários de programação e, para cada um, indique se a melhor escolha é uma função pura ou não pura, com uma justificativa curta.

escolhas = {
    "Cálculo de média para vários sistemas": (
        "Função pura",
        "Facilita testes e reutilização.",
    ),
    "Perguntar nome e cumprimentar no terminal": (
        "Função não pura",
        "Interação direta com usuário é o objetivo.",
    ),
    "Regra de desconto que muda com frequência": (
        "Função pura",
        "Isola regra de negócio e reduz acoplamento.",
    ),
}
for cenario, (tipo, motivo) in escolhas.items():
    print(f"Cenário: {cenario}")
    print(f"Escolha: {tipo}")
    print(f"Justificativa: {motivo}")
    print("-")

Exemplo de execução:

Cenário: Cálculo de média para vários sistemas
Escolha: Função pura
Justificativa: Facilita testes e reutilização.
-
Cenário: Perguntar nome e cumprimentar no terminal
Escolha: Função não pura
Justificativa: Interação direta com usuário é o objetivo.
-
Cenário: Regra de desconto que muda com frequência
Escolha: Função pura
Justificativa: Isola regra de negócio e reduz acoplamento.
-

O dicionário associa cada cenário a uma tupla com dois textos: o tipo de função recomendado e o motivo. No for, o .items() entrega a cada volta a chave e a tupla, e a expressão cenario, (tipo, motivo) desempacota as duas de uma vez: cenario recebe a chave, e os parênteses separam a tupla em tipo e motivo.