Python 允许通过 exec 和 eval 执行以字符串形式表示的代码片段,这体现了动态语言的特性。利用这种特性,可以让代码变得更灵活。不过一直以来,我对这种"动态"的用法不太"适应",因为:
- 让代码引入了某些不安定因素,这些代码片段执行后可能对全局造成影响。尤其是当使用全局名称空间时,它的作用范围难以控制。
- 对执行的效率也有影响。Python 在执行代码之前也是要预编译的,比如 pyc 文件。因此这些字符串形式的代码片段在执行的时候,需要编译的过程,哪怕是使用 compile 编译后重复使用,第一次的编译是难以避免的。
很多人也倾向于避免这种用法,比如:Use of eval in Python?。
不过最近在写网站解析程序的时候,发现为了实现目的,最好的解决方案还是引入这种字符串形式的代码片段。这个问题具体描述是:我需要对一系列类似的网站进行解析。整体的解析过程是相似的,所以我建立了一个框架,使用 XML 保存每个站点各自的解析属性,通常是 XPath 和正则表达式。但是某些内容各个网站的表现方式有很大差别,这些内容想要解析出来只使用 XPath 和正则表达式是不够的。因此对这些例外情形,最好的方式是直接利用代码片段来计算。
此外,现有的项目,比如 Genshi 之类模板引擎也采用了类似的处理手段。
用户使用程序如果能执行自己的代码片段,这往往是有潜在危险的,尤其是在网络服务中,因此我们在使用的字符串代码片段的时候,要严格限制名称空间。对这个问题的讨论,可以参考 Using eval() safely in python,这里做一个简要的总结。
默认的,eval 和 exec 所运行的代码都位于当前的名称空间中,它们也可以接受一个或两个可选字典参数作为代码执行的全局名称空间和局部名称空间。
eval 的用法最严格的限制(注意对 builtins 的处理)是:
eval(user_func,{"__builtins__":None},{"x":x,"sin":sin})
如果希望允许使用某些变量和函数,可以采用下面的方法:
eval(user_func,{"__builtins__":None},{"x":x,"sin":sin})
有关使用 exec 和 eval 的注意事项,文章 Be careful with exec and eval in Python 进行了细致的讨论。首次编译是不可避免的,使用预编译后重复使用可以提高效率。另外使用局部空间因此可以加快变量的查询速度,所以执行会快。
下面还有几个需要注意的地方:
能避免使用的时候,还是应该采用其他方式,比如转换字符串的时候,使用 int() 而不是 eval(),因为如果转换的这个字符串是用户输入的,危险的情况是 eval() 会执行恶意输入的条命令。详细的解释参考:Python之eval()函数的危险。
尽量避免使用 eval 来获取变量名,想要实现动态变量名,使用 globals(),locals() 以及 vars()。
a = 123
s1 = locals()['a']
s2 = vars()['a']
print s1, s2