Closures em Python
Aprenda como funcionam os closures em Python: funções aninhadas, variáveis capturadas, nonlocal, armadilhas comuns e casos de uso reais com exemplos executáveis.
Um closure é uma função aninhada que lembra as variáveis do escopo envolvente no qual foi definida, mesmo após a função externa ter retornado. Closures permitem que você anexe estado privado a uma função sem usar uma classe — tornando-os uma das ferramentas mais elegantes do Python para construir callbacks, fábricas e auxiliares com estado.
Esta página cobre como os closures funcionam, as três condições que eles exigem, armadilhas comuns e casos de uso práticos.
O que é um Closure?
Quando Python executa uma função, ele cria um escopo local que desaparece assim que a função retorna. Normalmente, qualquer variável definida ali desaparece. Um closure é a exceção: se uma função interna referencia uma variável de uma função externa, Python mantém essa variável viva em um objeto cell especial, e a função interna carrega uma referência a essas cells para onde quer que vá.
O closure mais simples é uma fábrica de funções — uma função que constrói e retorna outra função:
def make_multiplier(factor):
def multiply(n):
return n * factor # 'factor' is captured from the enclosing scope
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
print(double(10)) # 20Cada chamada a make_multiplier cria um closure novo com sua própria cópia independente de factor. double e triple são completamente independentes, mesmo tendo sido criados pela mesma função.
Três Condições para um Closure
Uma função é um closure quando todas estas três condições são verdadeiras:
- Existe uma função aninhada — uma função definida dentro de outra função.
- A função aninhada referencia uma variável do escopo envolvente — essa variável é chamada de variável livre.
- A função envolvente retorna a função aninhada (ou a passa para outro lugar).
def outer():
message = 'Hello from outer' # free variable
def inner():
print(message) # inner references it
return inner # outer returns inner
greet = outer()
greet() # Hello from outerApós outer() retornar, seu frame local desaparece — mas message sobrevive dentro de greet.__closure__.
Inspecionando um Closure
Python expõe as cells de closure por meio do atributo __closure__:
def make_adder(n):
def add(x):
return x + n
return add
add5 = make_adder(5)
print(add5(3)) # 8
print(add5.__closure__) # (<cell at 0x...>,)
print(add5.__closure__[0].cell_contents) # 5__closure__ é uma tupla de objetos cell — um por variável capturada. Se uma função não é um closure, __closure__ é None.
Modificando Variáveis Capturadas com nonlocal
Por padrão, você pode ler uma variável capturada, mas não pode religar ela. Tentar atribuir a ela cria uma nova variável local em vez disso, o que geralmente não é o que você quer. Use a palavra-chave nonlocal para dizer ao Python que você se refere à variável do escopo envolvente:
def make_counter(start=0):
count = start
def increment(step=1):
nonlocal count # rebind the enclosing 'count', not a new local
count += step
return count
return increment
counter = make_counter()
print(counter()) # 1
print(counter()) # 2
print(counter(5)) # 7
counter2 = make_counter(10)
print(counter2()) # 11
print(counter()) # 8 — counter is unaffectedCada chamada a make_counter produz uma cell count independente. counter e counter2 não compartilham estado.
Para uma análise mais aprofundada de como Python decide a qual escopo uma variável pertence, veja Python Scope.
Armadilha Comum: Closures em Loops
Um erro clássico é criar closures dentro de um loop e esperar que cada um capture o valor atual da variável do loop:
# Wrong — all functions capture the same 'i' cell
funcs = []
for i in range(3):
funcs.append(lambda: i)
print([f() for f in funcs]) # [2, 2, 2] — not [0, 1, 2]Todos os três lambdas compartilham uma cell que guarda a variável do loop i. Quando são chamados, i já chegou ao seu valor final de 2.
Correção 1: Argumento padrão (captura por valor)
funcs = []
for i in range(3):
funcs.append(lambda i=i: i) # default arg is evaluated immediately
print([f() for f in funcs]) # [0, 1, 2]Correção 2: Função fábrica
def make_func(i):
def f():
return i
return f
funcs = [make_func(i) for i in range(3)]
print([f() for f in funcs]) # [0, 1, 2]A função fábrica cria um escopo novo — e, portanto, uma cell nova — para cada iteração. Esta é a abordagem mais explícita e legível.
Casos de Uso Práticos
Aplicação Parcial
Closures são uma alternativa leve a functools.partial quando você precisa de uma versão pré-configurada de uma função:
def make_power(exponent):
def power(base):
return base ** exponent
return power
square = make_power(2)
cube = make_power(3)
print(square(4)) # 16
print(cube(3)) # 27Memoização Simples
Um closure pode guardar um dicionário de cache que persiste entre as chamadas:
def make_memoized(func):
cache = {}
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
@make_memoized
def slow_square(n):
return n * n
print(slow_square(4)) # 16
print(slow_square(4)) # 16 (served from cache)
print(slow_square(7)) # 49Este padrão é exatamente como os Decorators do Python funcionam internamente — um decorator é apenas um closure que envolve outra função.
Configuração de Callbacks
Closures são úteis para construir callbacks que precisam de um pouco de contexto incorporado:
def make_logger(prefix):
def log(message):
print(f'[{prefix}] {message}')
return log
info = make_logger('INFO')
error = make_logger('ERROR')
info('Server started') # [INFO] Server started
error('Disk full') # [ERROR] Disk fullClosures vs. Classes
Um closure e uma classe com um único método frequentemente resolvem o mesmo problema. Escolha com base na complexidade:
| Situação | Prefira |
|---|---|
| Um estado, um comportamento | Closure |
| Múltiplos métodos ou atributos públicos | Classe |
| Precisa ser serializado (ex.: pickle) | Classe |
| Passando um callback para outra função | Closure |
# Class approach
class Counter:
def __init__(self, start=0):
self.count = start
def increment(self, step=1):
self.count += step
return self.count
# Closure approach
def make_counter(start=0):
count = start
def increment(step=1):
nonlocal count
count += step
return count
return incrementAmbos produzem comportamento idêntico. O closure é mais curto; a classe é mais descobrível e extensível.
Conclusão
Closures permitem que uma função aninhada carregue seu próprio estado privado ao lembrar as variáveis do escopo onde foi definida. Os pontos principais são:
- Um closure requer uma função aninhada, uma variável livre e a função interna sendo retornada ou passada adiante.
- Use
nonlocalquando precisar religar (não apenas ler) uma variável capturada. - Evite a armadilha da variável de loop: use uma função fábrica ou um argumento padrão para capturar o valor em cada iteração.
- Closures são a base dos decorators e estão intimamente relacionados às regras de escopo do Python.