【问题标题】:Captured variables in "eval" in PythonPython中“eval”中捕获的变量
【发布时间】:2014-12-24 14:46:06
【问题描述】:

我无法理解 Python 中“eval()”和“exec”的语义。 (此问题中的所有代码在 Python 2.7.8 和 Python 3.4.2 中的行为方式相同)。 “eval”的documentation 表示:

如果 [locals 和 globals] 都省略,则执行表达式 在调用 eval() 的环境中。

“exec”也有类似的语言。我显然不明白这句话,因为我希望下面程序定义的四个函数做同样的事情。

def h(x):
    ls = locals()
    exec('def i(y): return (w, x, y)', globals(), ls)
    i = ls['i']
    def       j(y): return (w, x, y)
    k = eval('lambda y: (w, x, y)')
    l =       lambda y: (w, x, y)
    return i, j, k, l

w = 1

i, j, k, l = h(2)

他们没有。

>>> i(3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 1, in i
NameError: name 'x' is not defined
>>> j(3)
(1, 2, 3)
>>> k(3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 1, in <lambda>
NameError: name 'x' is not defined
>>> l(3)
(1, 2, 3)

反汇编代码揭示了原因:“x”被“eval”和“exec”视为全局变量。

from dis import dis
print("This is `i`:")
dis(i)
print("This is `j`:")
dis(j)
print("This is `k`:")
dis(k)
print("This is `l`:")
dis(l)
print("For reference, this is `h`:")
dis(h)

输出:

This is `i`:
  1           0 LOAD_GLOBAL              0 (w)
              3 LOAD_GLOBAL              1 (x)
              6 LOAD_FAST                0 (y)
              9 BUILD_TUPLE              3
             12 RETURN_VALUE        
This is `j`:
 25           0 LOAD_GLOBAL              0 (w)
              3 LOAD_DEREF               0 (x)
              6 LOAD_FAST                0 (y)
              9 BUILD_TUPLE              3
             12 RETURN_VALUE        
This is `k`:
  1           0 LOAD_GLOBAL              0 (w)
              3 LOAD_GLOBAL              1 (x)
              6 LOAD_FAST                0 (y)
              9 BUILD_TUPLE              3
             12 RETURN_VALUE        
This is `l`:
 27           0 LOAD_GLOBAL              0 (w)
              3 LOAD_DEREF               0 (x)
              6 LOAD_FAST                0 (y)
              9 BUILD_TUPLE              3
             12 RETURN_VALUE        
For reference, this is `h`:
 22           0 LOAD_NAME                0 (locals)
              3 CALL_FUNCTION            0
              6 STORE_FAST               1 (ls)

 23           9 LOAD_CONST               1 ('def i(y): return (w, x, y)')
             12 LOAD_NAME                1 (globals)
             15 CALL_FUNCTION            0
             18 LOAD_FAST                1 (ls)
             21 EXEC_STMT           

 24          22 LOAD_FAST                1 (ls)
             25 LOAD_CONST               2 ('i')
             28 BINARY_SUBSCR       
             29 STORE_FAST               2 (i)

 25          32 LOAD_CLOSURE             0 (x)
             35 BUILD_TUPLE              1
             38 LOAD_CONST               3 (<code object j at 0x7ffc3843c030, file "test.py", line 25>)
             41 MAKE_CLOSURE             0
             44 STORE_FAST               3 (j)

 26          47 LOAD_NAME                2 (eval)
             50 LOAD_CONST               4 ('lambda y: (w, x, y)')
             53 CALL_FUNCTION            1
             56 STORE_FAST               4 (k)

 27          59 LOAD_CLOSURE             0 (x)
             62 BUILD_TUPLE              1
             65 LOAD_CONST               5 (<code object <lambda> at 0x7ffc3843c3b0, file "test.py", line 27>)
             68 MAKE_CLOSURE             0
             71 STORE_FAST               5 (l)

 28          74 LOAD_FAST                2 (i)
             77 LOAD_FAST                3 (j)
             80 LOAD_FAST                4 (k)
             83 LOAD_FAST                5 (l)
             86 BUILD_TUPLE              4
             89 RETURN_VALUE

问题

“j”和“l”上面有我想要的行为。如何使用“eval”或“exec”获得这种行为?

失败 1

使用类而不是函数作为外包装确实会改变语义,但与期望的方式相反。它将“x”变成一个全局变量。

class H:
    x = 2
    f = staticmethod(eval('lambda y: (w, x, y)'))

H.dis(H.f)

w = 1
H.f(3)

输出:

  1           0 LOAD_GLOBAL              0 (w)
              3 LOAD_GLOBAL              1 (x)
              6 LOAD_FAST                0 (y)
              9 BUILD_TUPLE              3
             12 RETURN_VALUE        
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 1, in <lambda>
NameError: global name 'x' is not defined

包装在“classmethod”中或将其保留为未绑定的实例方法只会让事情变得更糟。

失败 2

使用字符串插值替换“x”对整数有效:

def h(x):
    return eval('lambda y: (w, %r, y)' % x)

k = h(2)

dis(k)

w = 1
k(3)

输出:

  1           0 LOAD_GLOBAL              0 (w)
              3 LOAD_CONST               1 (2)
              6 LOAD_FAST                0 (y)
              9 BUILD_TUPLE              3
             12 RETURN_VALUE        
(1, 2, 3)

但是,我不想假设“x”可以无损地转换为字符串并返回。尝试在以下示例中被破坏:

k = h(lambda: "something")

k = h(open('some_file', 'w'))

cell = ["Wrong value"]
k = h(cell)
cell[0] = "Right value"
k(3)

失败 3

由于 Python 正在寻找全局变量,一个明显的尝试是将“x”作为全局变量传递:

def h(x):
    my_globals = {'w': w, 'x': x}
    return eval('lambda y: (w, x, y)', my_globals)

k = h(2)

dis(k)

w = 1
k(3)

输出:

  1           0 LOAD_GLOBAL              0 (w)
              3 LOAD_GLOBAL              1 (x)
              6 LOAD_FAST                0 (y)
              9 BUILD_TUPLE              3
             12 RETURN_VALUE        
(1, 2, 3)

这个尝试被破坏了,因为它过早地读取了“w”的值:

w = "Wrong value"
k = h(2)
w = "Right value"
k(3)

成功1

我最终确实找到了一种可行的方法,但我真的不喜欢它:

def h(x):
    return eval('lambda x: lambda y: (w, x, y)')(x) 

k = h(2)

dis(k)

w = 1
k(3)

输出:

  1           0 LOAD_GLOBAL              0 (w)
              3 LOAD_DEREF               0 (x)
              6 LOAD_FAST                0 (y)
              9 BUILD_TUPLE              3
             12 RETURN_VALUE
(1, 2, 3)

尤其是,如果我不知道传递给“eval”的字符串捕获的局部变量的完整列表,这将变得很痛苦。

你能做得更好吗?

2014 年 12 月 25 日更新

失败 4

寻找更多创建局部变量“x”的方法,我尝试了这个:

def h(x):
    ls = locals()
    exec('x = x\ndef i(y): return (w, x, y)', globals(), ls)
    exec('_ = x\ndef j(y): return (w, x, y)', globals(), ls)
    return ls['i'], ls['j'], ls['_'], ls['x']

i, j, check1, check2 = h(2)

assert check1 == 2
assert check2 == 2

w = 1

print("This is `i`:")
dis(i)
print("This is `j`:")
dis(j)

print("i(3) = %r" % (i(3),))
print("j(3) = %r" % (j(3),))

对“x”的额外赋值无效。断言验证“x”在本地字典中,但它没有被 lambdas 捕获。这是输出:

This is `i`:
  2           0 LOAD_GLOBAL              0 (w)
              3 LOAD_GLOBAL              1 (x)
              6 LOAD_FAST                0 (y)
              9 BUILD_TUPLE              3
             12 RETURN_VALUE
This is `j`:
  2           0 LOAD_GLOBAL              0 (w)
              3 LOAD_GLOBAL              1 (x)
              6 LOAD_FAST                0 (y)
              9 BUILD_TUPLE              3
             12 RETURN_VALUE

对“i”和“j”的调用都崩溃了,抱怨没有全局变量“x”。

成功2

[编辑 2014-12-29:这仅在 Python 3 上成功。]

另一种创建局部变量的方法是这样的:

def h(x):
    i = eval('[lambda y: (w, x, y) for x in [x]][0]')
    j = eval('[lambda y: (w, x, y) for _ in [x]][0]')
    return i, j

i, j = h(2)

w = 1

print("This is `i`:")
dis(i)
print("This is `j`:")
dis(j)

print("i(3) = %r" % (i(3),))
print("j(3) = %r" % (j(3),))

奇怪的是,在这种情况下,对“x”的额外分配确实有效果。这确实有效,即“i”与“j”不同。这是输出:

This is `i`:
  1           0 LOAD_GLOBAL              0 (w)
              3 LOAD_DEREF               0 (x)
              6 LOAD_FAST                0 (y)
              9 BUILD_TUPLE              3
             12 RETURN_VALUE
This is `j`:
  1           0 LOAD_GLOBAL              0 (w)
              3 LOAD_GLOBAL              1 (x)
              6 LOAD_FAST                0 (y)
              9 BUILD_TUPLE              3
             12 RETURN_VALUE
i(3) = (1, 2, 3)

对“j”的调用崩溃,抱怨没有全局“x”,但“i”按预期工作并且具有正确的字节码。

为什么这行得通,而上面的“失败 4”却行不通?确定本地“x”是否可以被捕获的规则是什么?这种设计的历史是什么? (这似乎很荒谬!)

【问题讨论】:

  • ij 不是本地人。它们是闭包。 Python 在编译时确定闭包,但在eval() 中产生这种闭包的唯一方法是您已经发现的方式。您不能以任何其他方式创建闭包。
  • 令人惊讶的是,“locals”中传递的变量无法被捕获。这是为什么呢?
  • 因为闭包需要更多的脚手架;变量的生命周期超出了函数的生命周期,并且常规的局部变量不会在那里。
  • 对,您找到了另一种创建新范围的方法;列表推导(以及 dict 和集合推导以及生成器表达式)也允许您在单个表达式中创建父范围。
  • 您也可以使用默认参数破解。在 PEP 227 将词法作用域添加到 Python 2.1 之前,这很常见。您可以使用 3.x 仅关键字参数使其更可靠:k = eval('lambda y,*,x=x: (w,x,y)')

标签: python python-2.7 eval python-3.4


【解决方案1】:

我认为您希望您创建的函数继承创建它们的函数的本地环境,但真正的全局环境(创建它们的函数的)。这就是为什么你不喜欢他们将 x 称为全局,对吧?

下面创建了一个围绕所需函数的“包装器”函数,所有这些都在同一个 exec 字符串中。 当你调用或重新调用包装器时传入创建函数的本地值,创建一个新的包装闭包。

代码对在本地上下文中创建的新变量很敏感。确保函数和包装器名称都是已知的并且在那里有值会很麻烦。

def wrap_f(code, gs, ls, wrapper_name, function_name):
    ls[function_name] = ls[wrapper_name] = None
    arglist = ",".join(ls.keys())
    wrapcode = """
def {wrapper_name}({arglist}):
{code}
    return {function_name}
    """.format(wrapper_name=wrapper_name, arglist=arglist, 
               code=code, function_name=function_name)
    exec(wrapcode, gs, ls)
    wrapper = ls[wrapper_name]
    return wrapper, wrapper(**ls)

所以,要回答原来的问题,这段代码……

def h(x):
    mcode = "    def m(y): return (w, x, y)"  # must be indented 4 spaces.
    mwrap, m = wrap_f(mcode, globals(), locals(), "mwrap", "m")
    return m

w = 1
m = h(2)
print m(3)

...产生这个输出:

(1, 2, 3)

这个例子展示了当创建者函数中的本地人发生变化时该怎么做:

def foo(x):
    barleycode = """
    def barley(y):
        print "barley's x =", x
        print "barley's y =", y
    """
    barleywrap, barley = wrap_f(barleycode, globals(), locals(), 
                               "barleywrap", "barley")
    barley("this string")
    print

    x = "modified x"
    barley = barleywrap(**locals())
    barley("new arg")
    print

    x = "re-modified x"
    barley("doesn't see the re-mod.")

x = "the global x"

foo("outer arg")

这会产生输出:

barley's x = outer arg
barley's y = this string

barley's x = modified x
barley's y = new arg

barley's x = modified x
barley's y = doesn't see the re-mod.

【讨论】:

  • "我认为你希望你创建的函数继承创建它们的函数的本地环境,但也继承真正的全局环境(创建它们的函数)。这就是你不喜欢的原因他们将 x 称为全局,对吗?”
  • 我认为我不需要知道“我传递给“eval”的字符串捕获的局部变量的完整列表”。我只需要知道其中的一个超集,'locals().keys()' 就可以了。
  • 按照我的说法,你不需要知道哪些是需要的,你只需要拥有所有可能需要的。这些变量名都进入传递给 eval 的字符串的“barleywrap”部分。
  • apt1002 -- 我编辑了答案,以便在您编写问题时包含对问题的更直接的答案。如果您认为是,请选中“这是一个答案”按钮!
【解决方案2】:

我不确定我自己是否完全明白,但我会尽力而为: 我想当你运行 eval/exec python 时不明白它在函数内部,我真的不知道为什么。 我会尝试使用这样的格式字符串

k = eval("lambda y: (w, {0}, y)".format(x))

我不确定这东西是否有效。 还有,为什么要这样使用eval和exec呢?

【讨论】:

    【解决方案3】:

    我将分享我自己的理解,为什么 python 会有这样的行为

    Lambda 如何捕获引用

    每当一个变量不在使用的参数列表中时,python 都会为它创建一个 闭包。例如

    y
    def h(x):
        l =lambda y: (w, x,y)
    

    创建一个捕获 x 的闭包,您可以通过访问来检查它

    l.__closure__
    

    这将显示 x 与函数创建一起存储。 但是,y 与函数一起存储,因为它被定义为全局变量

    类定义

    这会在运行A.f()时导致名称错误

    class A:
        c = 1
        f = lambda :c+1
    

    因为 python 会在没有定义 c 的全局命名空间中寻找 c

    原因

    Python 3's doc to exec function

    如果 exec 获得两个单独的对象作为 globalslocals,则代码 将像嵌入到 class 定义中一样执行。

    这说明了为什么 lambda 不捕获本地命名空间中的变量

    解决方法

    k = eval('lambda y: (w, x, y)',dict(globals(),**))
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-08-20
      • 1970-01-01
      • 1970-01-01
      • 2020-04-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多