Funções avançado em Python
LidoEsta aula explora conceitos avançados de funções em Python: funções como objetos de primeira classe, funções aninhadas, closures e recursão. Você aprenderá a criar código mais flexível e elegante, com exemplos práticos e exercícios.
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
-
Escreva uma função
aplicar_operacaoque 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 -
Crie uma função externa
multiplicador_porque 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 -
Escreva uma função recursiva
soma_recursivaque 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 -
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 -
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