将代码存储在块中作为稍后在该范围内执行的函数

时间:2013-10-16 05:58:10

标签: python scope decorator contextmanager

这似乎是可能无法实现的,但我正在尝试实现这样的事情:

a = 0
with before():
  a += 1

do_thing(a) # does thing with a, whose value is now 1
do_thing(a) # does thing with a, whose value is now 2

所以我想要一些可以在with语句中使用块的东西,在某个地方保存该块并在每个do_thing函数调用之前调用它,同时使用该范围。

另一种选择是这样的:

@before
def callback():
  a += 1

而不是with声明。我认为,这两个选项对我都没问题,尽管首选的是with语句。

我认为有些东西应该做我想要的,here,但是当我真正尝试它时会出现错误。

1 个答案:

答案 0 :(得分:1)

您可以创建一个装饰器,将函数存储在列表中,由另一个装饰器附加到另一个函数。这似乎是你认为问题的难点,但它是微不足道的:

before_funcs = []
def before(func):
    before_funcs.append(func)
    return func

def attach_befores(func):
    @functools.wraps(func)
    def newfunc(*args, **kwargs):
        for before_func in before_funcs:
            before_func()
        return func(*args, **kwargs)
    return newfunc

所以,现在你可以这样做:

a = 0

@before
def callback():
    global a
    a += 1

@before
def another():
    global a
    a *= 2

@attach_befores
def do_thing(i):
    print(i)

请注意,那里需要global a,因为该功能无效。


现在,你可以称之为:

do_thing(a)
do_thing(a)
do_thing(a)
do_thing(a)

但是,它不会给你想要的结果 - 特别是,更改全局a不会更改传递给真实do_thing函数的参数。为什么?因为在调用函数之前会计算函数参数。因此,在已经评估了参数之后重新绑定a对您没有好处。当然,它仍会改变传递给 next 调用的参数。所以,输出将是:

0
2
6
14

如果你只是想修改传递给函数的参数,那么你不需要对全局变量进行所有这些操作。只需让before函数修改参数,让装饰器应用程序通过每个before函数传递参数,然后再将它们传递给实函数。

或者,或者,如果你想修改函数使用的全局变量,让函数实际使用那些全局变量而不是参数。

或者,或者,如果你想改变它的位置,使它成为可变的,就像列表一样,并使before函数改变值,而不是仅仅将全局重新绑定到不同的值。 / p>

但是你所要求的是一个可以达到调用框架的装饰器,找出为获取参数而评估的表达式,并强制它们被重新评估。那太傻了。


如果你真的真的想要这样做,那么唯一的方法就是捕获并解释sys._getframe(1).f_code中的字节码。

至少在CPython 2.7中,你将得到一些代码序列,将你的修饰函数推送到堆栈上(在典型的情况下是一个简单的LOAD_NAMELOAD_NAME,但不一定),然后是用于评估表达式的代码序列,然后是CALL_FUNCTION / CALL_FUNCTION_VAR /等。因此,您可以向后走,模拟操作,直到找到将函数推入堆栈的操作。 (我不确定如何以万无一失的方式执行此操作,但它应该是可行的。然后,构建一个新的code对象,只需用LOAD_CONST推送您的函数并重复所有操作在它之后(然后返回值)。然后将code包装在function中,其环境与调用者完全相同,然后调用该新函数并返回其值,而不是调用包装函数直接

以下是一个例子:

def call_do_thing(b):
    global a
    b += a
    return do_thing(a * b)

伪字节码是:

LOAD_FAST b
LOAD_GLOBAL a
INPLACE_ADD
STORE_FAST b
LOAD_GLOBAL do_thing
LOAD_GLOBAL a
LOAD_FAST b
BINARY_MULTIPLY
CALL_FUNCTION 1
RETURN_VALUE

在这种情况下,查找函数调用很简单,因为它使用了LOAD_GLOBAL。所以,我们只需要将所有操作从那里带到RETURN_VALUE并将它们包装在一个新函数中,而不是我们给出的那个,并且a将被重新评估在新的全局中。