17

为什么将变量作为全局变量或局部变量传递给 Python 的函数eval()会有所不同会有什么不同?

文档中所述,Python 将复制__builtins__到全局变量。但肯定还有其他一些我看不到的区别。

考虑以下示例函数。它接受一个字符串code并返回一个函数对象。不允许使用内置函数(例如),但允许使用包中abs()的所有函数。math

def make_fn(code):
    import math
    ALLOWED_LOCALS = {v:getattr(math, v)
        for v in filter(lambda x: not x.startswith('_'), dir(math))
    }
    return eval('lambda x: %s' % code, {'__builtins__': None}, ALLOWED_LOCALS)

它按预期工作,不使用任何本地或全局对象:

   fn = make_fn('x + 3')
   fn(5) # outputs 8

但它不适用于以下math功能:

   fn = make_fn('cos(x)')
   fn(5)

这会输出以下异常:

   <string> in <lambda>(x)
   NameError: global name 'cos' is not defined

但是当传递与全局相同的映射时,它可以工作:

def make_fn(code):
   import math
   ALLOWED = {v:getattr(math, v)
      for v in filter(lambda x: not x.startswith('_'), dir(math))
   }
   ALLOWED['__builtins__'] = None
   return eval('lambda x: %s' % code, ALLOWED, {})

与上面相同的示例:

   fn = make_fn('cos(x)')
   fn(5) # outputs 0.28366218546322625

这里详细发生了什么?

4

1 回答 1

11

默认情况下,Python 将名称查找为全局变量;只有分配给函数的名称才会被查找为局部变量(因此任何作为函数参数或在函数中分配的名称)。

当您使用该dis.dis()函数反编译代码对象或函数时,您可以看到这一点:

>>> import dis
>>> def func(x):
...     return cos(x)
... 
>>> dis.dis(func)
  2           0 LOAD_GLOBAL              0 (cos)
              3 LOAD_FAST                0 (x)
              6 CALL_FUNCTION            1
              9 RETURN_VALUE        

LOAD_GLOBALcos作为全局名称加载,仅在全局名称空间中查找。操作码使用当前LOAD_FAST命名空间(函数局部变量)按索引查找名称(函数局部命名空间经过高度优化并存储为 C 数组)。

还有另外三个操作码来查找名称;LOAD_CONST(为真正的常量保留,例如None不可变值的字面量定义),LOAD_DEREF(引用闭包)和LOAD_NAME. 后者确实同时查看局部变量和全局变量,并且仅在无法优化功能代码对象时使用,因为LOAD_NAME速度要慢得多。

如果您真的cos在 中查找locals,则必须强制代码未优化;这适用于 Python 2,通过添加exec()调用(或exec语句):

>>> def unoptimized(x):
...     exec('pass')
...     return cos(x)
... 
>>> dis.dis(unoptimized)
  2           0 LOAD_CONST               1 ('pass')
              3 LOAD_CONST               0 (None)
              6 DUP_TOP             
              7 EXEC_STMT           

  3           8 LOAD_NAME                0 (cos)
             11 LOAD_FAST                0 (x)
             14 CALL_FUNCTION            1
             17 RETURN_VALUE        

NowLOAD_NAME用于cos因为所有 Python 都知道,该exec()调用将该名称添加为本地名称。

即使在这种情况下,localsLOAD_NAME也将是函数本身的 locals,而不是传递给 的 locals eval,它们仅用于父范围。

于 2013-08-28T12:40:51.813 回答