关于python:深度辨析-Python-的-eval-与-exec

3次阅读

共计 5149 个字符,预计需要花费 13 分钟才能阅读完成。

Python 提供了很多内置的工具函数(Built-in Functions),在最新的 Python 3 官网文档中,它列出了 69 个。

大部分函数是咱们常常应用的,例如 print()、open() 与 dir(),而有一些函数尽管不罕用,但它们在某些场景下,却能施展出不个别的作用。内置函数们可能被“提拔”进去,这就意味着它们皆有独到之处,有用武之地。

因而,把握内置函数的用法,就成了咱们应该点亮的技能。

。这篇文章是一份超级具体的学习记录,零碎、全面而深刻地辨析了这两大函数。

1、eval 的根本用法

语法:eval(expression, globals=None, locals=None)

它有三个参数,其中 expression 是一个字符串类型的表达式或代码对象,用于做运算;globals 与 locals 是可选参数,默认值是 None。

具体而言,expression 只能是单个表达式,不反对简单的代码逻辑,例如赋值操作、循环语句等等。(PS:单个表达式并不意味着“简略有害”,参见下文第 4 节)

globals 用于指定运行时的全局命名空间,类型是字典,缺省时应用的是以后模块的内置命名空间。locals 指定运行时的部分命名空间,类型是字典,缺省时应用 globals 的值。两者都缺省时,则遵循 eval 函数执行时的作用域。值得注意的是,这两者不代表真正的命名空间,只在运算时起作用,运算后则销毁。

x = 10

def func():
    y = 20
    a = eval('x + y')
    print('a:', a)
    b = eval('x + y', {'x': 1, 'y': 2})
    print('x:' + str(x) + 'y:' + str(y))
    print('b:', b)
    c = eval('x + y', {'x': 1, 'y': 2}, {'y': 3, 'z': 4})
    print('x:' + str(x) + 'y:' + str(y))
    print('c:', c)

func()

输入后果:

a:  30
x: 10 y: 20
b:  3
x: 10 y: 20
c:  4

由此可见,当指定了命名空间的时候,变量会在对应命名空间中查找。而且,它们的值不会笼罩理论命名空间中的值。

2、exec 的根本用法

语法:exec(object[, globals[, locals]])

在 Python2 中 exec 是个语句,而 Python3 将其革新成一个函数,就像 print 一样。exec() 与 eval() 高度类似,三个参数的意义和作用相近。

次要的区别是,exec() 的第一个参数不是表达式,而是代码块,这意味着两点:一是它不能做表达式求值并返回进来,二是它能够执行简单的代码逻辑,相对而言性能更加弱小,例如,当代码块中赋值了新的变量时,该变量可能 在函数外的命名空间中存活下来。

>>> x = 1
>>> y = exec('x = 1 + 1')
>>> print(x)
>>> print(y)
2
None

能够看出,exec() 内外的命名空间是相通的,变量由此传递进来,而不像 eval() 函数,须要一个变量来接管函数的执行后果。

3、一些细节辨析

两个函数都很弱小,它们将字符串内容当做无效的代码执行。这是一种字符串驱动的事件,意义重大。然而,在理论应用过程中,存在很多渺小的细节,此处就列出我所晓得的几点吧。

常见用处:将字符串转成相应的对象,例如 string 转成 list,string 转成 dict,string 转 tuple 等等。

>>> a = "[[1,2], [3,4], [5,6], [7,8], [9,0]]"
>>> print(eval(a))
[[1, 2], [3, 4], [5, 6], [7, 8], [9, 0]]
>>> a = "{'name':'Python 猫 ','age': 18}"
>>> print(eval(a))
{'name': 'Python 猫', 'age': 18}

# 与 eval 略有不同
>>> a = "my_dict = {'name':'Python 猫 ','age': 18}"
>>> exec(a)
>>> print(my_dict)
{'name': 'Python 猫', 'age': 18}

eval() 函数的返回值是其 expression 的执行后果,在某些状况下,它会是 None,例如当该表达式是 print() 语句,或者是列表的 append() 操作时,这类操作的后果是 None,因而 eval() 的返回值也会是 None。

>>> result = eval('[].append(2)')
>>> print(result)
None

exec() 函数的返回值只会是 None,与执行语句的后果无关,所以,将 exec() 函数赋值进来,就没有任何必要。所执行的语句中,如果蕴含 return 或 yield,它们产生的值也无奈在 exec 函数的内部起作用。

>>> result = exec('1 + 1')
>>> print(result)
None

两个函数中的 globals 和 locals 参数,起到的是白名单的作用,通过限定命名空间的范畴,避免作用域内的数据被滥用。

conpile() 函数编译后的 code 对象,可作为 eval 和 exec 的第一个参数。compile() 也是个神奇的函数。

吊诡的部分命名空间:后面讲到了 exec() 函数内的变量是能够扭转原有命名空间的,然而也有例外。

def foo():
    exec('y = 1 + 1\nprint(y)')
    print(locals())
    print(y)

foo()

依照后面的了解,预期的后果是局部变量中会存入变量 y,因而两次的打印后果都会是 2,然而实际上的后果却是:

2
{'y': 2}
Traceback (most recent call last):
...(略去局部报错信息)
    print(y)
NameError: name 'y' is not defined

明明看到了部分命名空间中有变量 y,为何会报错说它未定义呢?

起因与 Python 的编译器无关,对于以上代码,编译器会先将 foo 函数解析成一个 ast(形象语法树),而后将所有变量节点存入栈中,此时 exec() 的参数只是一个字符串,整个就是常量,并没有作为代码执行,因而 y 还不存在。直到解析第二个 print() 时,此时第一次呈现变量 y,但因为没有残缺的定义,所以 y 不会被存入部分命名空间。

在运行期,exec() 函数动静地创立了局部变量 y,然而因为 Python 的实现机制是“运行期的部分命名空间不可扭转”,也就是说这时的 y 始终无奈成为部分命名空间的一员,当执行 print() 时也就报错了。

至于为什么 locals() 取出的后果有 y,为什么它不能代表真正的部分命名空间?为什么部分命名空间无奈被动静批改?

若想把 exec() 执行后的 y 取出来的话,能够这样:z = locals()[‘y’],然而如果不小心写成了上面的代码,则会报错:

def foo():
    exec('y = 1 + 1')
    y = locals()['y']
    print(y)
    
foo()

#报错:KeyError: 'y'
#把变量 y 改为其它变量则不会报错 

KeyError 指的是在字典中不存在对应的 key。本例中 y 作了申明,却因为循环援用而无奈实现赋值,即 key 值对应的 value 是个有效值,因而读取不到,就报错了。

此例还有 4 个变种,我想用一套自恰的说法来解释它们,但尝试了很久,未果。留个后话吧,等我想明确,再独自写一篇文章。
4、为什么要慎用 eval()?

很多动静的编程语言中都会有 eval() 函数,作用大同小异,然而,无一例外,人们会通知你说,防止应用它。

为什么要慎用 eval() 呢?次要出于平安思考,对于不可信的数据源,eval 函数很可能会招来代码注入的问题。

>>> eval("__import__('os').system('whoami')")
desktop-fa4b888\pythoncat
>>> eval("__import__('subprocess').getoutput('ls ~')")
#后果略,内容是以后门路的文件信息 

在以上例子中,我的隐衷数据就被裸露了。而更可怕的是,如果将命令改为 rm -rf ~,那当前目录的所有文件都会被删除洁净。

针对以上例子,有一个限度的方法,即指定 globals 为 {‘__builtins__’: None} 或者 {‘__builtins__’: {}}。

>>> s = {'__builtins__': None}
>>> eval("__import__('os').system('whoami')", s)
#报错:TypeError: 'NoneType' object is not subscriptable

builtins 蕴含了内置命名空间中的名称,在控制台中输出 dir(builtins),就能发现很多内置函数、异样和其它属性的名称。在默认状况下,eval 函数的 globals 参数会隐式地携带__builtins__,即便是令 globals 参数为 {} 也如此,所以如果想要禁用它,就得显式地指定它的值。
上例将它映射成 None,就意味着限定了 eval 可用的内置命名空间为 None,从而限度了表达式调用内置模块或属性的能力。

然而,这个方法还不是十拿九稳的,因为仍有伎俩能够发动攻打。

某位破绽开掘高手在他的博客中分享了一个思路,令人大开眼界。其外围的代码是上面这句,你能够试试执行,看看输入的是什么内容。

>>> ().__class__.__bases__[0].__subclasses__()

对于这句代码的解释,以及更进一步的利用伎俩,详见博客。(地址:www.tuicool.com/articles/je…

另外还有一篇博客,不仅提到了上例的伎俩,还提供了一种新的思路:

正告:千万不要执行如下代码,后果自负。

>>> eval('(lambda fc=(lambda n: .__subclasses__() if c.__name__ == n][0]):fc("function")(fc("code")(0,0,0,0,"KABOOM",(),(),(),"","",0,""),{})())()', {"__builtins__":None})

这行代码会导致 Python 间接 crash 掉。具体分析在:segmentfault.com/a/119000001…

除了黑客的伎俩,简略的内容也能发动攻打。像下例这样的写法,将在短时间内耗尽服务器的计算资源。

>>> eval("2 ** 888888888", {"__builtins__":None}, {})

如上所述,咱们直观地展现了 eval() 函数的危害性,然而,即便是 Python 高手们小心谨慎地应用,也不能保障不出错。

在官网的 dumbdbm 模块中,已经(2014 年)发现一个安全漏洞,攻击者通过伪造数据库文件,能够在调用 eval() 时发动攻打。(详情:bugs.python.org/issue22885)

独一无二,在上个月(2019.02),有外围开发者针对 Python 3.8 也提出了一个平安问题,提议不在 logging.config 中应用 eval() 函数,目前该问题还是 open 状态。(详情:bugs.python.org/issue36022)

如此种种,足以阐明为什么要慎用 eval() 了。同理可证,exec() 函数也得审慎应用。

5、平安的代替用法

既然有种种安全隐患,为什么要发明出这两个内置办法呢?为什么要应用它们呢?

理由很简略,因为 Python 是一门灵便的动静语言。与动态语言不同,动静语言反对动静地产生代码,对于曾经部署好的工程,也能够只做很小的部分批改,就实现 bug 修复。

那有什么方法能够绝对平安地应用它们呢?

ast 模块的 literal() 是 eval() 的平安代替,与 eval() 不做查看就执行的形式不同,ast.literal() 会先查看表达式内容是否无效非法。它所容许的字面内容如下:

strings, bytes, numbers, tuples, lists, dicts, sets, booleans, 和 None

一旦内容非法,则会报错:

import ast
ast.literal_eval("__import__('os').system('whoami')")

报错:ValueError: malformed node or string

不过,它也有毛病:AST 编译器的栈深(stack depth)无限,解析的字符串内容太多或太简单时,可能导致程序解体。

至于 exec(),仿佛还没有相似的代替办法,毕竟它自身可反对的内容是更加简单多样的。

最初是一个倡议:搞清楚它们的区别与运行细节(例如后面的部分命名空间内容),审慎应用,限度可用的命名空间,对数据源作充沛校验。

正文完
 0