Python 代码动态执行初探

Posted songofhawk

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python 代码动态执行初探相关的知识,希望对你有一定的参考价值。

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

以上是关于Python 代码动态执行初探的主要内容,如果未能解决你的问题,请参考以下文章

是否可以动态编译和执行 C# 代码片段?

TorchDynamo初探:Python ByteCode的动态修改

scrapy按顺序启动多个爬虫代码片段(python3)

数据流分析初探

如何在 python 中并行化以下代码片段?

javascript 3d网页 简单几行代码创建一个动态水潭, 湖面 示例 ( three.js r114 初探 四)