头图

Python 代码动态执行初探

songofhawk
English

作为“动态”语言,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
阅读 682

hawk
关注创业公司的技术与团队
222 声望
7 粉丝
0 条评论
你知道吗?

222 声望
7 粉丝
宣传栏