首頁 >後端開發 >Python教學 >Python中eval() 與exec() 函數的用法解析

Python中eval() 與exec() 函數的用法解析

不言
不言轉載
2019-03-25 09:55:243163瀏覽

這篇文章帶給大家的內容是關於Python中eval() 與exec() 函數的用法解析,有一定的參考價值,有需要的朋友可以參考一下,希望對你有所幫助。

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() 也是個神奇的函數,我翻譯的上一篇文章《Python騷操作:動態定義函數》就示範了一個動態定義函數的操作。

吊詭的局部命名空間:前面講到了 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,为什么它不能代表真正的局部命名空间?为什么局部命名空间无法被动态修改?可以查看我之前分享的《Python 动态赋值的陷阱》,另外,官方的 bug 网站中也有对此问题的讨论,查看地址:https://bugs.python.org/issue...

若想把 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__()

关于这句代码的解释,以及更进一步的利用手段,详见。(地址:http://www.php.cn/python-tutorials-416494.html

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

#警告:千万不要执行如下代码,后果自负。
>>> eval('(lambda fc=(lambda n: [c 1="c" 2="in" 3="().__class__.__bases__[0" language="for"][/c].__subclasses__() if c.__name__ == n][0]):fc("function")(fc("code")(0,0,0,0,"KABOOM",(),(),(),"","",0,""),{})())()', {"__builtins__":None})

这行代码会导致 Python 直接 crash 掉。具体分析在:http://www.php.cn/python-tutorials-416495.html

除了黑客的手段,简单的内容也能发起攻击。像下例这样的写法, 将在短时间内耗尽服务器的计算资源。

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

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

在官方的 dumbdbm 模块中,曾经(2014年)发现一个安全漏洞,攻击者通过伪造数据库文件,可以在调用 eval() 时发起攻击。(详情:https://bugs.python.org/issue...)

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

如此种种,足以说明为什么要慎用 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() ,似乎还没有类似的替代方法,毕竟它本身可支持的内容是更加复杂多样的。

最后是一个建议:搞清楚它们的区别与运行细节(例如前面的局部命名空间内容),谨慎使用,限制可用的命名空间,对数据源作充分校验。

本篇文章到这里就已经全部结束了,更多其他精彩内容可以关注PHP中文网的python视频教程栏目!

以上是Python中eval() 與exec() 函數的用法解析的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:segmentfault.com。如有侵權,請聯絡admin@php.cn刪除