作为“动态”语言,Python在运行时加载一段代码并执行,肯定是比需要编译的“静态语言”(比如C,Java)要方便多了。
执行方式
可以按是否返回结果,简单分为两种:exec和eval。
exec
exec负责执行字符串代码,可支持多行,可定义变量,但无法返回结果
def pr(x):
print('My result: {}'.format(x))
if __name__ == "__main__":
s = '''
a = 15
b = 3
if a > b:
pr(a+b)
'''
exec(s)
执行结果> My result: 18
eval
eval可以返回结果,但只能执行单行表达式
def select_max(x, y):
return x if x > y else y
if __name__ == "__main__":
a = 3
b = 5
c = eval('select_max(a , b)')
print("c is {}".format(c))
执行结果> c is 5
运行时环境
从上面的代码示例可以看出,无论exec还是eval,它们的运行环境,就跟调用它们位置的代码一样:无论是全局的函数,还是局部的变量,只要在执行指定代码前定义过,就可以使用,并且exec中定义的变量,也可以被后面的代码引用。
如果有必要,我们也可以在运行动态代码的时候,指定环境定义的内容,从而增加和屏蔽一些信息。exec与evel,都不止一个参数,他们的第二个和第三个参数,分别可以指定动态代码的globals与locals环境。
所谓globals,就是代码执行时的全局环境,可以通过globals()函数获取,返回结果是个dict,列出了所有全局变量和全局方法,包括用import的导入的模块和方法;同理,locals()函数能返回所有局部变量和局部方法。
而我们调用动态代码的时候,如果像下面这样传参:
def select_max(x, y):
return x if x > y else y
c = eval('select_max(3 , 5)', {}, {})
就会覆盖掉缺省的globals(第2个参数)和locals(第3个参数)设定,只能使用buildin的方法了,此时上面的代码就会报错——因为找不到select_max方法。为了让这个方法可用,我们需要给其中某个dict赋值:
def select_max(x, y):
return x if x > y else y
c = eval('select_max(3 , 5)', {'select_max':select_max}, {})
这看起来有点像脱了裤子放屁:明明直接用就好,为什么先覆盖掉,再赋一遍值呢?其实是出于安全性考虑。
安全性
动态代码能力,通常是暴露给程序外部的,让配置人员可以扩展程序逻辑。但是如果不加限制,这个能力也是很危险的:比如通过调用open方法,可以打开任意文件,删除其内容。
所以比较安全的方式,是把内置函数也禁用,只暴露允许外部调用的方法:
def select_max(x, y):
return x if x > y else y
c = eval('select_max(3 , 5)', {"__builtins__": {}}, {'select_max':select_max})
注意:网上有很多文章,把__builtins__设置为None,经实测,至少在Python 3.7环境中是不可行的,应当设置为空字典
优化
编译
上面提到的两种方式示例,都是直接执行字符串。我们肯定可以想到,这些字符串再执行前,会先被python运行时“解析”(parsing)一遍,而解析的过程是很耗时的。所以,为了优化效率,可以先用compile函数,预先“编译”好,把字符串变成“代码”,每次执行的效率就会大大提高。
def select_max(x, y):
return x if x > y else y
if __name__ == "__main__":
exp = compile('select_max(a , b)', '', 'eval')
for i in range(10):
a = i
b = i + 10
c = eval(exp)
print("c is {}".format(c))
可以看出来,字符串经过compile之后,变成了表达式,之后在循环中反复调用该表达式,会比每次解析字符串,效率高得多。同样,exec执行的内容,也可以先用compile编译为表达式。
注意:compile的第二个参数,是文件名(可以直接从文件读取代码),如果没有可直接置空
编译的环境
compile 和 eval/exec 可以不在同一个函数中被调用,那么它们拥有的执行环境就不一样,但实际上compile并不检查环境,动态代码中用到的变量或方法,在编译时完全可以不存在。比如把上面的代码改成下面的样子:
exp = compile('select_max(a , b)', '', 'eval')
def select_max(x, y):
return x if x > y else y
if __name__ == "__main__":
for i in range(10):
a = i
b = i + 10
c = eval(exp)
print("c is {}".format(c))
在定义select_max方法之前,就编译表达式,完全不影响运行效果:
c is 10
c is 11
c is 12
c is 13
c is 14
c is 15
c is 16
c is 17
c is 18
c is 19
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。