Capítulo 18, Básico
Escopo e armadilhas das funções
Onde uma variável existe, e as duas armadilhas que fazem mais gente perder tempo: variável local mal usada e valor padrão mutável.
Código deste capítulo: basico/cap18_escopo.py
Escopo: onde um nome vale
O Python procura um nome em quatro lugares, nesta ordem, que a comunidade chama de LEGB: Local (dentro da função), Enclosing (função externa), Global (módulo) e Built-in (nomes embutidos, como len).
mensagem = "global"
def externa():
mensagem = "local da externa"
def interna():
print(mensagem)
interna()
print(mensagem)
externa()
print(mensagem)
local da externa
local da externa
global
Para alterar uma variável global dentro de uma função, é preciso declarar global. Eu evito: estado global torna o código difícil de testar e de entender. Prefira receber o valor como parâmetro e devolver o novo:
contador = 0
def incrementar():
global contador
contador += 1
incrementar()
incrementar()
print(contador)
2
UnboundLocalError
Se você atribui a um nome dentro da função, o Python o trata como local na função inteira, inclusive nas linhas anteriores à atribuição. Por isso o erro abaixo acontece mesmo existindo um total global:
total = 10
def quebrado():
total += 1
try:
quebrado()
except UnboundLocalError as erro:
print(erro)
cannot access local variable 'total' where it is not associated with a value
O valor padrão mutável
Esta é a armadilha mais famosa da linguagem. O valor padrão de um parâmetro é criado uma única vez, quando a função é definida. Se for uma lista, todas as chamadas compartilham a mesma lista:
def adicionar(item, lista=[]):
lista.append(item)
return lista
print(adicionar(1))
print(adicionar(2))
print(adicionar.__defaults__)
[1]
[1, 2]
([1, 2],)
A segunda chamada devolve [1, 2], embora você não tenha passado lista nenhuma: o 1 ficou guardado no próprio valor padrão. A solução é usar None como sentinela e criar a lista dentro da função:
def adicionar_correto(item, lista=None):
if lista is None:
lista = []
lista.append(item)
return lista
print(adicionar_correto(1))
print(adicionar_correto(2))
[1]
[2]
A regra que eu sigo: nunca use lista, dicionário ou conjunto como valor padrão. Linters como o ruff acusam esse erro automaticamente (regra B006).
Efeitos colaterais
Uma função que altera os argumentos que recebe tem um efeito colateral. Isso não é proibido, mas deve ser explícito no nome e na documentação. Compare:
def ordenar_no_lugar(lista):
lista.sort()
def ordenada(lista):
return sorted(lista)
original = [3, 1, 2]
nova = ordenada(original)
print(original, nova)
ordenar_no_lugar(original)
print(original)
[3, 1, 2] [1, 2, 3]
[1, 2, 3]
Recursão
Uma função pode chamar a si mesma. Toda função recursiva precisa de um caso base que pare a recursão. O Python limita a profundidade (1000 chamadas por padrão) e não otimiza recursão de cauda, então para problemas profundos prefira um laço:
import sys
def fatorial(n):
return 1 if n <= 1 else n * fatorial(n - 1)
print(fatorial(5))
print(sys.getrecursionlimit())
def sem_fim(n):
return sem_fim(n + 1)
try:
sem_fim(0)
except RecursionError:
print("limite de recursão atingido")
120
1000
limite de recursão atingido
Exercício 1
Corrija o bug do valor padrão
A função abaixo deveria devolver uma lista nova a cada chamada sem argumento. Corrija-a.
def registrar(evento, historico=[]):
historico.append(evento)
return historico