Nesta aula, vamos mergulhar em conceitos avançados de funções em Python. Você já sabe como definir e chamar funções, mas Python trata funções como objetos de primeira classe, o que abre um mundo de possibilidades: podemos atribuí-las a variáveis, passá-las como argumentos, retorná-las de outras funções e até criar funções dentro de funções. Vamos explorar esses recursos, além de closures e recursão, para escrever código mais expressivo e poderoso.

Esses tópicos são fundamentais para entender programação funcional em Python, design patterns como decorators e para resolver problemas de forma recursiva elegante. Ao final, você terá uma base sólida para avançar em tópicos como programação assíncrona e metaprogramação.

Funções como objetos

Em Python, tudo é objeto, inclusive funções. Isso significa que uma função pode ser tratada como qualquer outro valor: atribuída a uma variável, armazenada em estruturas de dados, passada como argumento para outras funções e retornada como resultado. Essa propriedade é conhecida como "funções de primeira classe" e é a base de muitos recursos avançados.

Quando você define uma função com def, o nome da função é uma referência para o objeto função. Você pode criar uma nova referência simplesmente atribuindo o nome a outra variável. Por exemplo:

def saudacao(nome):
    return f"Olá, {nome}!"

# Atribuindo a função a uma variável
minha_funcao = saudacao

# Chamando através da nova referência
print(minha_funcao("Maria"))  # Saída: Olá, Maria!

# Verificando que é o mesmo objeto
print(saudacao is minha_funcao)  # True

Funções podem ser armazenadas em listas, dicionários, etc. Por exemplo, podemos criar um dicionário que mapeia operações para funções:

def somar(a, b):
    return a + b

def subtrair(a, b):
    return a - b

def multiplicar(a, b):
    return a * b

operacoes = {
    '+': somar,
    '-': subtrair,
    '*': multiplicar
}

print(operacoes['+'](10, 5))   # 15
print(operacoes['*'](10, 5))   # 50

Além disso, funções podem ser passadas como argumento para outras funções (chamadas de funções de ordem superior). Um exemplo clássico é a função map, que recebe uma função e um iterável:

def dobro(x):
    return x * 2

numeros = [1, 2, 3, 4]
resultado = list(map(dobro, numeros))
print(resultado)  # [2, 4, 6, 8]

Essa capacidade permite criar funções genéricas e reutilizáveis, como aplicar_twice:

def aplicar_twice(func, valor):
    return func(func(valor))

def incrementar(x):
    return x + 1

print(aplicar_twice(incrementar, 5))  # 7 (incrementa duas vezes)

Funções aninhadas

Python permite definir funções dentro de outras funções. Essas funções internas são chamadas de funções aninhadas. Elas são úteis para encapsular lógica que não precisa ser exposta fora da função externa, e podem acessar variáveis da função externa (escopo léxico).

Vamos ver um exemplo simples:

def calcular_media(lista):
    def soma():  # função aninhada
        total = 0
        for item in lista:
            total += item
        return total
    return soma() / len(lista)

print(calcular_media([10, 20, 30]))  # 20.0

Funções aninhadas podem ser usadas para organizar código que só é relevante em um contexto específico. Elas também são a base para closures e decorators. Um ponto importante: a função interna não é acessível fora da função externa, a menos que seja retornada ou atribuída a uma variável externa.

Outro exemplo comum é usar funções aninhadas para criar funções auxiliares que dependem de parâmetros da função externa:

def criar_saudacao(prefixo):
    def saudacao(nome):
        return f"{prefixo}, {nome}!"
    return saudacao

bom_dia = criar_saudacao("Bom dia")
boa_noite = criar_saudacao("Boa noite")

print(bom_dia("João"))   # Bom dia, João!
print(boa_noite("Maria")) # Boa noite, Maria!

No exemplo acima, a função aninhada saudacao captura a variável prefixo da função externa. Quando retornamos a função interna, ela mantém esse contexto, formando uma closure (veremos a seguir).

Closures

Uma closure é uma função que lembra o ambiente onde foi criada, mesmo depois que a função externa terminou de executar. Em Python, isso acontece quando uma função interna referencia variáveis do escopo da função externa e é retornada ou armazenada de alguma forma.

No exemplo anterior, saudacao é uma closure porque captura a variável prefixo. Após a função criar_saudacao retornar, a variável prefixo ainda está acessível para a função saudacao.

Vamos ver outro exemplo mais clássico: um contador incremental.

def criar_contador():
    contador = 0
    def incrementar():
        nonlocal contador
        contador += 1
        return contador
    return incrementar

contador1 = criar_contador()
print(contador1())  # 1
print(contador1())  # 2

contador2 = criar_contador()
print(contador2())  # 1 (cada closure tem seu próprio estado)

Observe o uso da palavra-chave nonlocal para modificar a variável contador que pertence ao escopo da função externa. Sem nonlocal, a atribuição dentro de incrementar criaria uma variável local, e não alteraríamos o contador externo.

Closures são amplamente usadas em Python, por exemplo, em decorators e em funções que geram outras funções. Elas permitem criar funções com estado privado sem usar classes.

Recursão

Recursão é uma técnica onde uma função chama a si mesma para resolver um problema. É uma alternativa à iteração (loops) e é especialmente útil para problemas que podem ser quebrados em subproblemas menores e semelhantes, como árvores, fatoriais, sequência de Fibonacci, etc.

Toda função recursiva precisa de um caso base (condição de parada) para evitar recursão infinita. Sem caso base, a função chamaria a si mesma para sempre, causando um estouro de pilha (stack overflow).

Vamos ver um exemplo clássico: fatorial.

def fatorial(n):
    if n == 0:  # caso base
        return 1
    else:
        return n * fatorial(n - 1)

print(fatorial(5))  # 120

Na função acima, quando chamamos fatorial(5), a função chama fatorial(4), que chama fatorial(3), e assim por diante, até chegar ao caso base fatorial(0) = 1. Depois, as chamadas retornam e os resultados são multiplicados.

Outro exemplo é a sequência de Fibonacci:

def fibonacci(n):
    if n <= 1:
        return n
    else:
        return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(10))  # 55

Essa implementação é simples, mas ineficiente para valores grandes, pois recalcula os mesmos subproblemas muitas vezes. Para melhorar, podemos usar memoização (armazenar resultados já calculados) ou usar loops.

Recursão é poderosa, mas deve ser usada com cuidado. Em Python, a profundidade máxima de recursão é limitada (por padrão, 1000) para evitar estouro de pilha. Para problemas com profundidade maior, é melhor usar iteração ou aumentar o limite com sys.setrecursionlimit() (não recomendado).

Boas práticas e observações finais

  • Use funções como objetos para criar código mais flexível: passar funções como argumentos e retornar funções permite criar abstrações poderosas.
  • Prefira funções aninhadas para encapsular lógica interna: isso melhora a legibilidade e evita poluir o escopo global.
  • Closures são ótimas para criar funções com estado: mas lembre-se de que o estado é privado, acessível apenas pela closure.
  • Recursão deve ter sempre um caso base claro: e prefira soluções iterativas quando a recursão for muito profunda ou ineficiente.
  • Documente suas funções: especialmente as aninhadas e closures, para que outros programadores entendam o fluxo.

Exercícios

  1. Escreva uma função aplicar_operacao que recebe uma função e dois números, e retorna o resultado da função aplicada aos dois números. Teste com uma função lambda que soma dois números.

    ✓ Resposta:
    def aplicar_operacao(func, a, b):
        return func(a, b)
    
    soma = lambda x, y: x + y
    print(aplicar_operacao(soma, 10, 5))  # 15
    
  2. Crie uma função externa multiplicador_por que recebe um fator e retorna uma função que multiplica um número por esse fator. Use essa função para criar uma função que dobra números e teste com 5.

    ✓ Resposta:
    def multiplicador_por(fator):
        def multiplicar(numero):
            return numero * fator
        return multiplicar
    
    dobro = multiplicador_por(2)
    print(dobro(5))  # 10
    
  3. Escreva uma função recursiva soma_recursiva que calcula a soma de todos os números de 1 até n. Teste com n=5.

    ✓ Resposta:
    def soma_recursiva(n):
        if n == 0:
            return 0
        else:
            return n + soma_recursiva(n - 1)
    
    print(soma_recursiva(5))  # 15
    
  4. Use uma closure para criar uma função que conta quantas vezes foi chamada. Teste chamando a função três vezes e imprima o valor do contador.

    ✓ Resposta:
    def criar_contador():
        contador = 0
        def contar():
            nonlocal contador
            contador += 1
            return contador
        return contar
    
    contador = criar_contador()
    print(contador())  # 1
    print(contador())  # 2
    print(contador())  # 3
    
  5. Escreva uma função recursiva que calcule o n-ésimo termo da sequência de Fibonacci usando memoização (dica: use um dicionário para armazenar resultados). Teste com n=10.

    ✓ Resposta:
    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(10))  # 55
    

Referências