Decorator para Congelamento de Closures
por JoaoBueno em 16/06/2009
Melhor usar a breve definição do LucianoRamalho ontem na lista do que tentar descrever de novo:
"""Uma closure (clausura ou fechamento) é uma estrutura de dados que
armazena a definição de uma função juntamente com o ambiente onde ela foi definida, ou seja as variáveis que existem no momento em que é definida a função. Isso permite que a função acesse estas variáveis posteriormente, quando for invocada, mesmo que o escopo onde as variáveis foram definidas não exista mais.""" (LucianoRamalho, lista python-brasil, 15/06/2009)
Bom, um detalhe em Python é que as closures funcionam "bem demais" - e, com frequencia isso pode ser uma armadilha - por exemplo, ao se tentar uma lista de 10 funções, em que cada uma recebe um parâmetro e devolve esse parâmetro multiplicado pela sua posição na lista -
Pode-se pensar a principio em algo como:
def cria_vetor():
v = []
for i in range(10):
def f(x):
return x * i
v.append(f)
return v(eu sei que dá pra usar list comprehension ou lambdas, mas como estou apresentando um decorator aqui, não seria um bom exemplo)
Bom, acontece que todas as funções na lista v se comportam como se o valor de "i" fosse "9" - ou seja, o último valor assumido por "i" no contexto onde as funções "f" foram criadas:
>>> v[9](10) 90 >>> v[0](10) 90
Ordinariamente, isso é evitado envelopando-se cada função que vai existir fora do contexto onde foi criada, precisando de valores locais instântaneos, dentro de um contexto separado: ou seja, uma função de fábrica de funções que só existe momentaneamente para conter o closure de cada função que vai no vetor final:
def cria_vetor():
v = []
for i in range(10):
def g(i):
def f(x):
return x * i
return f
v.append(g(i))
return v(dica para a vida real (ex.: TKinter, QT4): verifique a função "partial" do módulo functools)
Ora - colocar uma função "wrapper" para modificar o comportamento de uma função desejada, é, a primeira vista, exatamente o motivo de decorators existirem.
Então resolvi criar o decorator freeze_context, que faz exatamente isso e funciona:
def cria_vetor():
v = []
for i in range(10):
@freeze_context
def f(x):
return x * i
v.append(f)
return v
Só que até chegar ai, tive que descer fundo em como o python cria as tais closures, e como contorna-las. Não foram poucas surpresas - entre as quais descobri que um dos atributos do objeto função, func_closures, é uma tupla de objetos "cell" que justamente contem os valores das variáveis no contexto externo _e_ não pode ser criado a partir de código python. (Quer dizer, não sem roubar).
Bom, se roubar é necessário para criar as tais "cells", então roubemos. O código do @freeze_context :
from types import FunctionType
from inspect import stack
# All this just to break again one
# of the things they surely strugled the most
# to fix in Python
def freeze_context(func):
#retrieve the context in which the original function was created
context_frame = stack()[-2][0]
if func.func_closure is None:
return func
# check the clousre vars and copy its values at the time
# of the function creation
freeze_variables = func.func_code.co_freevars
freeze_values = dict([(freevar, context_frame.f_locals[freevar])
for freevar in freeze_variables])
# now, we have to build a function within a closure in a string that uses
# the same variables and instaciate it with "exec" so that
# Python generates a func_closure tuple for us
factory_header = "def factory(%s):\n" % ",".join(freeze_variables)
factory_body = """ def func():\n return %s\n""" % ",".join(freeze_variables)
factory_footer = """ return func\n"""
factory_str = factory_header + factory_body + factory_footer
exec(factory_str)
trunk_cell_function = factory(**freeze_values)
reforged_function = FunctionType(func.func_code,
context_frame.f_globals,
func.func_name,
func.func_defaults,
trunk_cell_function.func_closure)
return reforged_function
def test_0(n):
v = []
for i in range(n):
@freeze_context
def f(x):
return i * x
v.append(f)
return v
v = test_0(10)
v[9](10)
v[2](10)Voltar ao CookBook


