如果将变量作为全局变量或本地变量传递给Python的函数eval(),为什么会有所不同?
同样described in the documenation,如果没有明确给出,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
但是当传递与globals相同的映射时,它可以工作:
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
这里有什么细节?
答案 0 :(得分:9)
默认情况下,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_GLOBAL
将cos
作为全局名称加载,仅查看全局命名空间。 LOAD_FAST
操作码使用当前命名空间(函数本地)按索引查找名称(函数本地命名空间经过高度优化并存储为C数组)。
还有三个操作码可以查找名称; LOAD_CONST
(保留为真常量,例如None
和不可变值的文字定义),LOAD_DEREF
(引用闭包)和LOAD_NAME
。后者确实查看了locals和globals,并且仅在无法优化函数代码对象时使用,因为LOAD_NAME
要慢得多。
如果你想要在cos
中查找locals
,那么你必须强制代码不被优化;通过添加exec()
调用(或exec
语句),此仅在Python 2中有效:
>>> 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
现在LOAD_NAME
用于cos
,因为对于所有Python都知道,exec()
调用将该名称添加为本地。
即使在这种情况下,本地LOAD_NAME
查看,将是函数本身的本地,而不是传递给eval
的本地,它们仅用于父作用域。