Python 提供了不少内置的工具函数(Built-in Functions),在最新的 Python 3 官方文档中,它列出了 69 个。css
大部分函数是咱们常用的,例如 print()、open() 与 dir(),而有一些函数虽然不经常使用,但它们在某些场景下,却能发挥出不通常的做用。内置函数们可以被“提拔”出来,这就意味着它们皆有独到之处,有用武之地。html
所以,掌握内置函数的用法,就成了咱们应该点亮的技能。node
在《Python进阶:如何将字符串常量转为变量?》这篇文章中,我提到过 eval() 和 exec() ,但对它们并不太了解。为了弥补这方面知识,我就从新学习了下。这篇文章是一份超级详细的学习记录,系统、全面而深刻地辨析了这两大函数。python
语法:eval(expression, globals=None, locals=None)数据库
它有三个参数,其中 expression 是一个字符串类型的表达式或代码对象,用于作运算;globals 与 locals 是可选参数,默认值是 None。express
具体而言,expression 只能是单个表达式,不支持复杂的代码逻辑,例如赋值操做、循环语句等等。(PS:单个表达式并不意味着“简单无害”,参见下文第 4 节)编程
globals 用于指定运行时的全局命名空间,类型是字典,缺省时使用的是当前模块的内置命名空间。locals 指定运行时的局部命名空间,类型是字典,缺省时使用 globals 的值。二者都缺省时,则遵循 eval 函数执行时的做用域。值得注意的是,这二者不表明真正的命名空间,只在运算时起做用,运算后则销毁。segmentfault
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
因而可知,当指定了命名空间的时候,变量会在对应命名空间中查找。并且,它们的值不会覆盖实际命名空间中的值。ruby
语法: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() 函数,须要一个变量来接收函数的执行结果。
两个函数都很强大,它们将字符串内容当作有效的代码执行。这是一种字符串驱动的事件 ,意义重大。然而,在实际使用过程当中,存在不少微小的细节,此处就列出我所知道的几点吧。
常见用途:将字符串转成相应的对象,例如 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/issue4831
若想把 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 个变种,我想用一套自恰的说法来解释它们,但尝试了好久,未果。留个后话吧,等我想明白,再单独写一篇文章。
不少动态的编程语言中都会有 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__()
关于这句代码的解释,以及更进一步的利用手段,详见博客。(地址:https://www.tuicool.com/articles/jeaqe2n)
另外还有一篇博客,不只提到了上例的手段,还提供了一种新的思路:
#警告:千万不要执行以下代码,后果自负。
>>> 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 掉。具体分析在:https://segmentfault.com/a/1190000011532358
除了黑客的手段,简单的内容也能发起攻击。像下例这样的写法, 将在短期内耗尽服务器的计算资源。
>>> eval("2 ** 888888888", {"__builtins__":None}, {})
如上所述,咱们直观地展现了 eval() 函数的危害性,然而,即便是 Python 高手们当心谨慎地使用,也不能保证不出错。
在官方的 dumbdbm 模块中,曾经(2014年)发现一个安全漏洞,攻击者经过伪造数据库文件,能够在调用 eval() 时发起攻击。(详情:https://bugs.python.org/issue22885)
无独有偶,在上个月(2019.02),有核心开发者针对 Python 3.8 也提出了一个安全问题,提议不在 logging.config 中使用 eval() 函数,目前该问题仍是 open 状态。(详情:https://bugs.python.org/issue36022)
如此种种,足以说明为何要慎用 eval() 了。同理可证,exec() 函数也得谨慎使用。
既然有种种安全隐患,为何要创造出这两个内置方法呢?为何要使用它们呢?
理由很简单,由于 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() ,彷佛尚未相似的替代方法,毕竟它自己可支持的内容是更加复杂多样的。
最后是一个建议:搞清楚它们的区别与运行细节(例如前面的局部命名空间内容),谨慎使用,限制可用的命名空间,对数据源做充分校验。
关联阅读:
https://docs.python.org/3/library/ast.html#ast.literal_eval
公众号【Python猫】, 专一Python技术、数据科学和深度学习,力图创造一个有趣又有用的学习分享平台。本号连载优质的系列文章,有喵星哲学猫系列、Python进阶系列、好书推荐系列、优质英文推荐与翻译等等,欢迎关注哦。PS:后台回复“爱学习”,免费得到一份学习大礼包。