shan

Python eval() - 动态执行表达式

2020-05-13

本文是一篇关于python eval() 函数的翻译文章,为个人学习使用。

原文地址:Python eval(): Evaluate Expressions Dynamically

进度:未完成

Python 的 eval() 方法允许你去执行任意的基于字符串 (string-based) 或者基于编译代码(compiled-code-based) 的 python 表达式。当你尝试动态的执行基于字符串或者编译代码对象输入的 python 表达式的时候,这个函数就会变得非常有用。

尽管 python 的 eval() 函数是一个非常有用的工具,但是在使用这个函数之前你需要知道,它同样有一些安全隐患。在这篇文档中,你会了解到 eval() 如何工作的以及如何在你的 python 程序中安全有效的使用它。

在这篇文档中,你将会了解

  • Python eval() 如何工作的
  • 怎样去使用 eval()动态执行基于字符串或者基于编译代码的任意输入
  • eval() 如何让你的代码变得不安全以及如何去最小化相关的安全风险

Understanding Python’s eval()

你可以使用 python 内置的函数 eval() 去动态执行基于字符串或者编译代码输入的表达式。如果你向eval() 方法传入一个字符串,那么接下来函数会去解析它,将它编译为字节码,然后作为Python表达式来执行。但是如果你用编译后的代码对象来调用 eval() 函数,那么函数就仅仅表现出执行步骤,如果你以相同的输入多次调用 eval() 方法,那么这么做是十分方便的。

Python eval() 的签名定义如下:

1
eval(expression[, globals[, locals]])

这个函数接受的第一个参数,被称作为expression,它保存着你需要执行的表达式。eval() 也接受两个可选参数:

  1. globals
  2. locals

在接下来的三个部分,你会了解到这是哪个参数是什么以及eval() 如何使用它们去动态执行 Python 表达式。

Note: 你也可以使用 exec() 去动态的执行 Python 的代码。eval()exec() 最主要的不同是 eval() 仅可以执行 Python 的表达式,而 exec() 可以执行任意的 Python 代码。

The First Argument: expression

eval() 的第一个参数被称为 expression. 它是必选参数,保存着传递给函数的基于字符串或者编译代码的输入。当你调用 eval() 时,expression 的内容作为 Python 表达式被执行。查看下面基于字符串输入的例子:

1
2
3
4
5
6
7
8
9
>>> eval("2 ** 8")
256
>>> eval("1024 + 1024")
2048
>>> eval("sum([8, 16, 32])")
56
>>> x = 100
>>> eval("x * 2")
200

当你使用字符串作为参数调用 eval() 的时候,函数的返回值是执行输入字符串得到的结果。因此,eval() 可以得到全局变量的值,比如上面例子中的 x

为了执行一个基于字符串的表达式,Python 的 eval() 会以下面的步骤运行:

  1. 解析表达式

    1. 将它编译为字节码
    2. 作为Python表达式进行执行
    3. 返回执行结果

变量 expression 作为 eval() 的第一个参数突出表达了函数仅和表达式一起工作而不是复合语句。Python文档中关于expression的定义如下:

expression

可以求出某个值的语法单元。 换句话说,一个表达式就是表达元素例如字面值、名称、属性访问、运算符或函数调用的汇总,它们最终都会返回一个值。 与许多其他语言不同,并非所有语言构件都是表达式。 还存在不能被用作表达式的 statement,例如 while。 赋值也是属于语句而非表达式。 (Source)

另一方面,一个Python的语句有如下的定义:

statement

语句是程序段(一个代码“块”)的组成单位。一条语句可以是一个 expression 或某个带有关键字的结构,例如 ifwhilefor。 (Source)

如果你尝试传递一个复合语句给 eval(), 然后就会产生一个 SyntaxError。查看下面的例子,在这个例子中你尝试使用 eval() 执行一个 if 语句

1
2
3
4
5
6
>>> x = 100
>>> eval("if x: print(x)")
File "<string>", line 1
if x: print(x)
^
SyntaxError: invalid syntax

如果你尝试使用 Python 的 eval() 方法执行一个复合语句,那么你会得到一个像上面的 trackback 一样的 SyntaxError。这是因为 eval() 仅仅只接受表达式(expressions)。任何其它的诸如 if, for, while, import, def, 或者 class 的语句, 都会产生一个error

Note: 一个 for 循环是一个复合语句,但是 for 关键字可以被用在推导式 ( comprehensions ) 中,这被认为是一个表达式。你可以使用 eval() 去执行推导式 ( comprehensions ) 即使它使用了 for 关键字。

赋值操作也同样不被允许用在 eval() 中。

1
2
3
4
5
>>> eval("pi = 3.1416")
File "<string>", line 1
pi = 3.1416
^
SyntaxError: invalid syntax

如果你尝试将赋值操作作为参数传递给 Python 的 eval() 函数,那么你会得到一个 SynctaxError。赋值操作是一个语句而不是表达式,并且语句是不被 eval() 允许的参数类型。

当解析器无法理解你输入的表达式时,你同样会得到一个 SynctaxError。接下来的例子中,尝试执行了一个违反 Python 语法的表达式。

1
2
3
4
5
6
>>> # Incomplete expression
>>> eval("5 + 7 *")
File "<string>", line 1
5 + 7 *
^
SyntaxError: unexpected EOF while parsing

你不能传递一个违反Python语法的表达式给 eval() 。在上面的例子中,你尝试执行一个不完整的表达式 ( “5 + 7 *” ) 然后你得到了一个 SynctaxError,因为解析器不理解这个表达式的语法。

你同样可以传递一个编译过的代码对象给 Python 的 eval() 方法。为了编译能够传递给 eval() 的代码,你可以使用 compile() 函数。这是一个内置的函数,它可以将一个输入的字符串编译为代码对象或者一个抽象语法树对象( AST object) ,因此你可以用 eval() 方法执行编译过的代码。

如何使用compile() 方法的细节超过了这篇文章的范围,但是这里速览一下它的前三个必备参数:

  1. source 保存了你想编译的源代码。这个参数接收通常的字符串字节字符以及 AST对象
  2. filename 给了读取代码的文件。如果你想使用一个基于字符串的输入,那么这个参数的值应该为 “”
  3. mode 具体指明了你想得到什么类型的编译代码。如果你想使用 eval() 处理编译后的代码,那么这个参数需要被设置为 “eval”

Note: 想获取更多关于 compile() 的信息,可以查看官方文档

你可以使用 compile() 将代码对象提供给 eval() ,而不是提供普通字符串。查看以下示例:

1
2
3
4
5
6
7
8
9
10
11
12
>>> # Arithmetic operations
>>> code = compile("5 + 4", "<string>", "eval")
>>> eval(code)
9
>>> code = compile("(5 + 7) * 2", "<string>", "eval")
>>> eval(code)
24
>>> import math
>>> # Volume of a sphere
>>> code = compile("4 / 3 * math.pi * math.pow(25, 3)", "<string>", "eval")
>>> eval(code)
65449.84694978735

如果你使用compile() 来编译要传递给eval() 的表达式,则eval() 会经历以下步骤:

  1. 执行编译后的代码
  2. 返回执行结果

如果你使用基于编译代码的输入调用 Python 的eval(),则该函数将进行执行步骤并立即返回结果。当你需要多次执行同一个表达式时,这会十分方便。在这种情况下,最好预先编译表达式,并在随后对 eval() 的调用中重用结果字节码。

如果你预先编译输入表达式,则对 eval() 的后续调用将运行得更快,因为你将不会重复解析编译步骤。如果你要执行复杂的表达式,不必要的重复会导致 CPU 时间增加以及过多的内存消耗。

The Second Argument: globals

eval() 的第二个参数被称为 globals 。它是一个可选参数,保存着一个字典类型。它为 eval() 函数提供一个全局的命名空间。有了 globals,你可以告诉 eval() 当执行 expression 时,哪个全局变量需要使用。

全局变量就是那些可以在你当前全局作用域或者命名空间中可以获得的变量。你可以在你代码的任意地方访问这些变量。

所有这些变量在一个字典中被传递给 globals ,当 eval() 执行的时候,可以获取到这些全局的变量。查看下面的例子,它展示了如何使用一个普通的字典去为 eval() 提供一个全局的命名空间。

1
2
3
4
5
6
7
8
9
>>> x = 100  # A global variable
>>> eval("x + 100", {"x": x})
200
>>> y = 200 # Another global variable
>>> eval("x + y", {"x": x})
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 1, in <module>
NameError: name 'y' is not defined

如果你为 eval() 的参数 globals 提供一个普通的字典,那么eval() 将只采用这个字典中的变量作为全局变量。任何其他定义在这个字典之外的全局变量都不能被 eval() 内部获得。这就是为什么当你在上面代码中尝试获取 y 时,Python 引发了一个 NameError:传递给 globals 的参数不包含 y

你可以通过在你的字典中列举这些变量,然后向 globals 中插入这些变量,然后这些变量在执行期间就可以获取了。例如,如果你将 y 插入 globals,那么上面例子中 “x + y” 的执行就会如预期的那样。

1
2
>>> eval("x + y", {"x": x, "y": y})
300

一旦你把 y 加到 globals 的字典中,那么 "x + y" 的执行结果就会成功并且返回你预期中的值 300。

你也可以提供在你全局作用域中不存在的变量。因此你需要为每个变量提供一个具体的值。eval() 在执行的时候会当做全局变量来解释这些变量。

1
2
3
4
5
6
>>> eval("x + y + z", {"x": x, "y": y, "z": 300})
600
>>> z
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'z' is not defined

即使你未在当前的全局作用域中定义 z,但该变量也存在于 globals 中,它的值为 300。在这种情况下,eval() 可以像访问全局变量一样访问 z

globals背后的机制非常灵活。你可以将任何可见变量 ( 全局,局部或非局部) 传递给全局变量。你也可以在上面的示例中传递自定义键值对,例如 "z":300eval() 会将它们全部视为全局变量。
关于全局变量重要的一点是,如果为其提供的自定义字典中不包含键”__builtins__“的值,则在解析表达式之前,将自动在 “__builtins__“ 下插入对内置字典的引用。这样可以确保 eval() 在执行表达式时将完全访问所有 Python 的内置变量。

以下示例显示,即使你为全局变量提供了一个空字典,对 eval() 的调用仍然可以访问 Python 的内置变量:

1
2
3
4
5
6
>>> eval("sum([2, 2, 2])", {})
6
>>> eval("min([1, 2, 3])", {})
1
>>> eval("pow(10, 2)", {})
100

在上面的代码中,你为 globals 提供了一个空字典({})。因此字典中不包含名称为 “__builtins__“ 的键,Python自动插入了一个 builtins 的引用。因此,当 eval() 解析 expression 时,它能够获取所有的Python的内置变量。

如果当你调用 eval() 时,没有传递一个字典给 globals 参数 ,那么该参数将默认为在调用 eval() 的环境中由globals() 返回的字典。

1
2
3
4
>>> x = 100  # A global variable
>>> y = 200 # Another global variable
>>> eval("x + y") # Access both global variables
300

当你在不提供 globals 参数的情况下调用 eval() 时,该函数将使用 globals()返回的字典作为其全局命名空间来执行 expression 。因此,在上面的示例中,你可以自由访问xy,因为它们是当前global作用域

The Third Argument: locals

Python 的 eval() 函数接收的第三个参数称为 locals 。这是另一个可选参数,它保存着一个字典。在这种情况下,字典保包含 eval() 执行 expression 时当做局部变量的变量。

局部变量就是那些你定义在一个给定函数内部的变量 (variables, functions, classes, and so on)。局部变量仅在函数内部可见。当你写一个函数的时候会定义这些类型的变量。

一旦 编写了 eval(),你不能向代码和局部作用域中添加局部变量。然而你可以传递一个字典给 locals, 然后 eval() 会把这些变量作为局部变量。

1
2
3
4
5
6
7
>>> eval("x + 100", {}, {"x": 100})
200
>>> eval("x + y", {}, {"x": 100})
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 1, in <module>
NameError: name 'y' is not defined

第二个字典在第一次调用 eval() 时会保存变量 x 。这个变量被 eval() 解释为一个局部变量。从另一方面来说,它被视为 eval() 主体中定义的变量。

你可以在 expression 中使用 x,并且 eval() 会接受这个参数。相反的,如果你想使用 y,那么你会得到一个NameError ,因为 y 并没有在 globals 或者 locals 的命名空间中定义。

就如 globals 一样,你可以传递任何可见的参数 (global, local, 或者 nolocal) 给 locals。就如上面的例子中一样,你同样可以传递如 “x”: 100 这样的键值对。eval() 会把他们都作为局部变量。

需要注意的是,当你想提供一个字典给 locals 时,你首先需要提供一个字典给 globals 。在eval() 中是无法使用关键字的。

1
2
3
4
>>> eval("x + 100", locals={"x": 100})
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: eval() takes no keyword arguments

如果你尝试在调用 eval() 时使用关键字参数,那么你会得到一个 TypeError,告诉你 eval() 不接受关键字参数。因此,你需要去提供一个 globals 参数的字典,然后才能使用 locals 的字典。

如果你不向 locals 提供一个字典,那么它默认指向传递给的字典。下面是一个关于你传递给 globals 一个空字典并且未传任何东西给 locals 的例子:

1
2
3
4
5
6
>>> x = 100
>>> eval("x + 100", {})
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 1, in <module>
NameError: name 'x' is not defined

从上面可以看出,当你不传任何字典给locals时,参数默认为传递给 globals 的字典。在这种情矿下,eval() 无法获得 x 的值,因为 globals 字典为空。

globalslocals 之间的主要实际区别在于,如果该 globalsbuiltins 键不存在,那么 Python会自动将 __builtins__ 这个键插入到 globals 中。无论你是否向globals提供自定义词典,都会发生这种情况。另一方面,如果你向locals提供自定义字典,那么在执行eval()期间该字典将保持不变。

Evaluating Expressions With Python’s eval()

你可以使用 Python eval() 函数区执行任何除了python语句 (statements) 的python表达式 (expression)。例如基于关键字的复合语句或者赋值语句。

eval() 函数在你需要动态执行python表达式的时候是非常有用的并且如果使用其他的python工具或者方法可能会增加你的开发时间或者精力。在这个部分,你会学到怎么使用python的 eval() 方法去执行 Boolean,math 以及普通目的的python表达式。

Boolean Expressions

Boolean 表达式是一个当解释器执行时返回真值 (True 或 False) 的 python 表达式。它们通常在 if 语句中执行。因为 Boolean 表达式不是复合语句,所以你可以使用 eval() 去执行它们。

1
2
3
4
5
6
7
8
9
10
>>> x = 100
>>> y = 100
>>> eval("x != y")
False
>>> eval("x < 200 and y > 100")
False
>>> eval("x is y")
True
>>> eval("x in {50, 100, 150, 200}")
True

你也可以在调用 eval() 过程中使用包含下面这些 Python 操作符的 Boolean 表达式:

在这种情况下,函数返回你执行的表达式的真值结果。

现在,你可能在想,为啥我们要使用eval() 而不是直接使用Boolean表达式?想一下,假设你需要实现一个条件语句,但是你想在执行过程中改变这个条件:

1
2
3
4
5
6
7
8
9
10
11
>>> def func(a, b, condition):
... if eval(condition):
... return a + b
... return a - b
...
>>> func(2, 4, "a > b")
-2
>>> func(2, 4, "a < b")
6
>>> func(2, 2, "a is b")
4

func() 中,你使用 eval() 去执行提供的条件,然后决定返回的结果是由 a+b 或者 a-b 中的一个。在上面的示例中,你仅使用了几种不同的条件,但是只要你坚持使用在func() 中定义的名称 ab,就可以使用任意数量的其他条件。

现在想象一下如何在不使用Python的eval()的情况下实现类似的功能。这样会减少代码和时间吗?肯定不行的!

Math Expressions

一个最常使用Python eval() 方法的地方就是计算基于字符输入的数学表达式。例如,当你想创造一个python 的计算器时,你就可以使用eval() 去执行用户的输入然后返回计算的结果。

下面就是一个使用 eval() 如何执行数学运算的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> # Arithmetic operations
>>> eval("5 + 7")
12
>>> eval("5 * 7")
35
>>> eval("5 ** 7")
78125
>>> eval("(5 + 7) / 2")
6.0
>>> import math
>>> # Area of a circle
>>> eval("math.pi * pow(25, 2)")
1963.4954084936207
>>> # Volume of a sphere
>>> eval("4 / 3 * math.pi * math.pow(25, 3)")
65449.84694978735
>>> # Hypotenuse of a right triangle
>>> eval("math.sqrt(math.pow(10, 2) + math.pow(15, 2))")
18.027756377319946

当你使用 eval() 去执行数学表达式的时候,你可以传递任何复杂程度的表达式。eval() 会解析它们,执行它们,如果一切正常的情况下,会返回你预期的结果。

General-Purpose Expressions

到目前为止,你知道了如何使用eval() 去执行Boolean和数学表达式。然而,你可以使用eval() 去执行更加复杂的Python表达式,包括函数调用,对象创建,属性获取,推导式等等。

例如,你可以调用一个内置函数或者你导入的标准库或者三方模块:

1
2
3
4
5
6
7
>>> # Run the echo command
>>> import subprocess
>>> eval("subprocess.getoutput('echo Hello, World')")
'Hello, World'
>>> # Launch Firefox (if available)
>>> eval("subprocess.getoutput('firefox')")
''

在这个例子中,你使用 Python eval() 去执行一些系统的命令。正如你想的那样,你可以使用这个特性做大量的有用的事情。然而,eval() 也会导致一系列的安全隐患,例如,允许一些恶意用户在你的机器上运行系统命令或者任意的代码。

在下一节,你会了解到在使用 eval() 时解决这些安全隐患的方法。

Minimizing the Security Issues of eval()

尽管Python的 eval() 方法非常的有用,但是它也会导致一些严重的安全隐患。eval() 是不够安全的,因为它允许你去动态的执行任意的Python代码。

这被认为是坏的编程习惯,因为你读取的或者写的代码不是你马上要执行的代码。如果你打算使用 eval() 方法去执行从某个用户或者其他外部来源的输入,那么你并不清楚什么样的代码会被执行。如果你的应用被错误的运行,那么这就会变为非常严重的安全隐患。

因为这个原因,好的编程习惯一般建议你不去使用eval() 方法。但是如果你决定无论如何都要使用这个方法,那么经验法则就是绝对不要使用不信任的输入。该规则的棘手部分是弄清楚你可以信任的输入类型。

举个例子说明如何不负责任地使用eval()会使你的代码不安全,假设你想构建一个在线服务来执行任意Python表达式。你的用户使用表达式,然后单击 运行 按钮。该应用程序会获取用户的输入并将其传递给eval()进行执行。

该应用程序将在你的个人服务器上运行。在这个服务器上拥有你的所有有价值的文件。如果你运行的是Linux机器,并且应用程序的进程具有正确的权限,则恶意用户可能会引入以下危险字符串:

1
"__import__('subprocess').getoutput('rm –rf *')"

上面的代码会删除这个应用当前目录下的所有文件。

*NOTE: * __import__() 是一个内置函数,它将模块名称作为字符串并返回对模块对象的引用。
__import__() 是一个函数,它与 import 语句完全不同。你无法使用eval() 执行import语句。

当输入不受信任时,没有完全有效的方法来避免与 eval() 相关的安全风险。但是,你可以通过限制eval() 的执行环境来最大程度地降低风险。在以下各节中,你将学习一些这样做的技术。

Restricting globals and locals

你可以通过将自定义字典传递给 globalslocals 参数限制 eval() 执行的环境。例如,你可以传递空字典给两个参数以防止eval() 访问调用者当前的作用域或者命名空间

1
2
3
4
5
6
7
>>> # Avoid access to names in the caller's current scope
>>> x = 100
>>> eval("x * 5", {}, {})
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 1, in <module>
NameError: name 'x' is not defined

如果你向 globalslocals 传递空字典 ({}),那么 eval() 在执行字符串 x * 5 时不会在全局命名空间或者它的局部命名空间中找到变量 x。作为结果,eval() 会抛出一个 NameError

不幸的是,像这样限制 globalslocals 参数并不能消除所有使用Python的 eval() 相关的所有安全风险,因为你仍然可以访问所有Python的内置变量。

Restricting the Use of Built-In Names

如前所述,Python的eval()在解析 expression 之前会自动将对 builtins 字典的引用插入 globals 中。恶意用户可以通过使用内置函数 __import__()来利用此行为,以访问标准库和系统上已安装的任何第三方模块。

下面例子说明,即使你已限制 globalslocals ,也可以使用任何内置函数和任何标准模块(如 mathsubprocess):

1
2
3
4
5
6
>>> eval("sum([5, 5, 5])", {}, {})
15
>>> eval("__import__('math').sqrt(25)", {}, {})
5.0
>>> eval("__import__('subprocess').getoutput('echo Hello, World')", {}, {})
'Hello, World'

即使你使用空字典限制了globalslocals,但是你仍然可以使用任何内置函数,就像在上面的代码中对sum()__import__()所做的一样。

你可以使用__import__()来导入任何标准或第三方模块,就像上面对mathsubprocess所做的一样。使用这种技术,你可以访问在 mathsubprocess或任何其他模块中定义的任何函数或类。现在想象一下,恶意用户可以使用 subprocess 或标准库中的任何其他功能强大的模块对你的系统进行操作。

为了最大程度地降低这种风险,你可以通过覆盖 globals 中的 __builtins__ 键来限制对Python内置函数的访问。较好的做法建议使用包含键值对 "__builtins__": {}的自定义词典。查看下面的例子:

1
2
3
4
5
>>> eval("__import__('math').sqrt(25)", {"__builtins__": {}}, {})
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 1, in <module>
NameError: name '__import__' is not defined

如果你将包含键值对"__builtins__": {}的字典传递给globals,则eval()将无法直接访问Python的内置函数,例如__import__()。但是,正如你将在下一节中看到的那样,这种方法仍无法完全使 eval() 安全。

Restricting Names in the Input

即使你可以使用自定义全局变量和局部变量字典来限制Python的 eval() 的执行环境,该函数仍然容易受到一些特殊技巧的攻击。例如,你可以使用类型字符如“”,[],{}或 () 以及一些特殊属性来访问类对象:

1
2
3
4
5
6
7
8
>>> "".__class__.__base__
<class 'object'>
>>> [].__class__.__base__
<class 'object'>
>>> {}.__class__.__base__
<class 'object'>
>>> ().__class__.__base__
<class 'object'>

一旦你访问了object后,你就可以使用特殊方法.__ subclasses__()来访问所有继承自object的类。方式如下:

1
2
3
4
5
6
7
8
9
>>> for sub_class in ().__class__.__base__.__subclasses__():
... print(sub_class.__name__)
...
type
weakref
weakcallableproxy
weakproxy
int
...

此代码会将大量的类列表打印到屏幕上。其中一些类非常强大,如果使用不当,可能会非常危险。这就打开了另一个重要的安全漏洞,你只是简单地限制eval()的执行环境是无法解决该漏洞的:

1
2
3
4
5
6
>>> input_string = """[
... c for c in ().__class__.__base__.__subclasses__()
... if c.__name__ == "range"
... ][0](10)"""
>>> list(eval(input_string, {"__builtins__": {}}, {}))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

上面代码中的列表推导式会过滤从 object 对象继承的类,返回包含 类 rangelist 。第一个索引([0])返回类 rassssnge。一旦可以访问 range,就可以调用它来生成 range 对象。然后,你在 range 对象上调用list() 以生成十个整数的列表。

在这个例子中,使用的range方法来说明 eval() 中的安全漏洞。想象一下,如果你的系统暴露了subprocess.Popen之类的类,恶意用户会做什么。

*Note: * 想要深入了解 eval() 方法的漏洞,请查看 Ned Batchelder 的文章:Eval确实很危险。该漏洞的一种可能解决方案是将输入中变量的使用限制为 safe名称或这没有任何变量。要实现此方法,你需要执行以下步骤:

  1. 创建包含要与eval()一起使用的名称的字典。
  2. 在模式 “eval” 下,使用 compile() 将输入字符串编译为字节码。
  3. 检查字节码对象上的 .co_names 以确保它仅包含允许的变量。
  4. 如果用户尝试输入不允许的变量,请引发一个 NameError

看一下实现所有这些步骤的函数:

1
2
3
4
5
6
7
8
9
10
11
>>> def eval_expression(input_string):
... # Step 1
... allowed_names = {"sum": sum}
... # Step 2
... code = compile(input_string, "<string>", "eval")
... # Step 3
... for name in code.co_names:
... if name not in allowed_names:
... # Step 4
... raise NameError(f"Use of {name} not allowed")
... return eval(code, {"__builtins__": {}}, allowed_names)

eval_expression()中,你实现了之前看到的所有步骤。该函数将可以与eval()一起使用的变量限制为仅字典allowed_names中的那些变量。为此,该函数使用.co_names,它是代码对象的一个属性,返回包含代码对象中的名称的元组

以下示例显示了eval_expression()在实际中的工作方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> eval_expression("3 + 4 * 5 + 25 / 2")
35.5
>>> eval_expression("sum([1, 2, 3])")
6
>>> eval_expression("len([1, 2, 3])")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 10, in eval_expression
NameError: Use of len not allowed
>>> eval_expression("pow(10, 2)")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 10, in eval_expression
NameError: Use of pow not allowed

如果你调用 eval_expression() 来执行算术运算,或者使用包含允许名称的表达式,则将获得预期的结果。否则,你会收到 NameError。在上述示例中,你唯一允许使用的名称是sum()。不允许使用诸如 len()pow() 之类的其他名称,因此当你尝试使用它们时,该函数会引发NameError

如果你想完全禁止使用名称,则可以如下重写eval_expression()

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> def eval_expression(input_string):
... code = compile(input_string, "<string>", "eval")
... if code.co_names:
... raise NameError(f"Use of names not allowed")
... return eval(code, {"__builtins__": {}}, {})
...
>>> eval_expression("3 + 4 * 5 + 25 / 2")
35.5
>>> eval_expression("sum([1, 2, 3])")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in eval_expression
NameError: Use of names not allowed

现在,你的函数不允许输入字符串中的任何名称。要做到这一点,你需要检查 .co_names 中的名称,如果发现一个名称,则会引发NameError。否则,你将执行input_string并返回执行结果。在这种情况下,你还可以使用空字典来限制locals

你可以使用此技术来最小化eval()的安全问题,并增强防御恶意攻击的能力。

Now your function doesn’t allow any names in the input string. To accomplish this, you check for names in .co_names and raise a NameError if one is found. Otherwise, you evaluate input_string and return the result of the evaluation. In this case, you use an empty dictionary to restrict locals as well.

You can use this technique to minimize the security issues of eval() and strengthen your armor against malicious attacks.

Restricting the Input to Only Literals

Python的eval()的一个常见用例是执行包含标准Python文字的字符串,并将其转换为具体对象。标准库提供了一个名为literal_eval()的函数,可以帮助实现此目标。

该函数不支持运算符,但支持列表,元组,数字,字符串等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> from ast import literal_eval
>>> # Evaluating literals
>>> literal_eval("15.02")
15.02
>>> literal_eval("[1, 15]")
[1, 15]
>>> literal_eval("(1, 15)")
(1, 15)
>>> literal_eval("{'one': 1, 'two': 2}")
{'one': 1, 'two': 2}
>>> # Trying to evaluate an expression
>>> literal_eval("sum([1, 15]) + 5 + 8 * 2")
Traceback (most recent call last):
...
ValueError: malformed node or string: <_ast.BinOp object at 0x7faedecd7668>

注意,literal_eval()仅适用于标准类型文字。它不支持使用运算符或变量。如果你尝试将表达式提供给 literal_eval(),则会收到 ValueError。此函数还可以帮助你将与使用Python的 eval() 相关的安全风险降至最低。

Using Python’s eval() With input()

Python 3.x中,内置的input()在命令行中读取用户输入,将其转换为字符串,剥离尾部的换行符,然后将结果返回给调用方。由于 input() 的结果是一个字符串,因此你可以将其输入到 eval() 并将其作为Python表达式求值:

1
2
3
4
5
6
>>> eval(input("Enter a math expression: "))
Enter a math expression: 15 * 2
30
>>> eval(input("Enter a math expression: "))
Enter a math expression: 5 + 8
13

你可以将 Python 的 eval() 包裹在 input() 之上,以自动执行用户的输入。这是 eval() 的常见用例,因为它模拟了 Python 2.x 中input()的行为,其中,input() 将用户的输入执行为 Python 表达式并返回结果。

由于其安全性,Python 2.x 中的 input() 行为已在 Python 3.x 中更改。

Building a Math Expressions Evaluator

到目前为止,你已经了解了 Python 的 eval() 的工作原理以及如何在实践中使用它。你还了解到 eval() 具有重要的安全隐患,并且通常被认为要避免在代码中使用 eval() 。但是,在某些情况下,Python的 eval() 可以为你节省大量时间和精力。

在本部分中,你会编写代码以动态执行数学表达式。如果你想在不使用 eval() 的情况下解决此问题,则需要执行以下步骤:

  1. 解析输入表达式。
  2. 将表达式的组件更改为Python对象(数字,运算符,函数等)。
  3. 将所有内容组合成一个表达式。
  4. 确认该表达式在Python中有效。
  5. 执行最终表达式并返回结果。

考虑到Python可以处理和执行的各种可能的表达式,这会花费大量的时间。幸运的是,你可以使用 eval() 解决此问题,并且你已经学习了几种降低相关安全风险的方法。

首先,创建一个名为 mathrepl.py 的 Python 脚本,然后添加以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 1 import math
2
3 __version__ = "1.0"
4
5 ALLOWED_NAMES = {
6 k: v for k, v in math.__dict__.items() if not k.startswith("__")
7 }
8
9 PS1 = "mr>>"
10
11 WELCOME = f"""
12 MathREPL {__version__}, your Python math expressions evaluator!
13 Enter a valid math expression after the prompt "{PS1}".
14 Type "help" for more information.
15 Type "quit" or "exit" to exit.
16 """
17
18 USAGE = f"""
19 Usage:
20 Build math expressions using numeric values and operators.
21 Use any of the following functions and constants:
22
23 {', '.join(ALLOWED_NAMES.keys())}
24 """

在这段代码中,你首先导入 Python 的 math 模块。该模块将允许你使用预定义的函数和常量执行数学运算。常量 ALLOWED_NAMES 保存一个字典,其中包含 math 中的非特殊名称。这样,你就可以将它们与eval()一起使用。

你还定义了三个字符串常量。你将使用它们作为脚本的用户界面,并根据需要将它们打印到屏幕上。

现在,你可以编写应用程序的核心功能了。在这种情况下,你需要编写一个函数来接收数学表达式作为输入并返回其结果。为此,你编写了一个名为evaluate()的函数:

1
2
3
4
5
6
7
8
9
10
11
26 def evaluate(expression):
27 """Evaluate a math expression."""
28 # Compile the expression
29 code = compile(expression, "<string>", "eval")
30
31 # Validate allowed names
32 for name in code.co_names:
33 if name not in ALLOWED_NAMES:
34 raise NameError(f"The use of '{name}' is not allowed")
35
36 return eval(code, {"__builtins__": {}}, ALLOWED_NAMES)

该函数的工作方式如下:

  1. 第26行中,定义evaluate()。该函数以字符串 expression 作为参数,并返回一个float,该值表示将字符串作为数学表达式求值的结果。
  2. 第29行中,使用 compile() 将输入字符串 expression 转换为已编译的 Python 代码。如果用户输入无效的表达式,则编译操作将引发SyntaxError
  3. 第32行中,启动 for 循环以检查 expression 中包含的名称,并确认可以在最终表达式中使用它们。如果用户提供的名称不在允许的名称列表中,则引发 NameError
  4. 第36行中,对数学表达式进行实际执行。注意,按照惯例,你将自定义字典传递给 globalslocalsALLOWED_NAMES 保存在 math 中定义的函数和常量。

注意:由于此应用程序使用了math中定义的函数,因此你需要考虑到当使用无效输入值调用它们时,其中一些函数会引发ValueError

例如,math.sqrt(-10)会引发错误,因为-10的平方根 未定义。

稍后,你将在客户端代码中看到如何捕获此错误。对globalslocals参数使用自定义值,以及对line33中的名称进行检查,可以使与eval()相关的安全风险降至最低。

当你在main()中编写其客户端代码时,你的数学表达式执行器将要完成。在此函数中,你将定义程序的主循环,并关闭读取和执行用户在命令行中输入的表达式的周期。

对于此示例,应用程序将:

  1. 向用户打印欢迎消息
  2. 显示提示,准备读取用户的输入
  3. 提供选项以获取使用说明并终止应用程序
  4. 读取用户的数学表达式
  5. 执行用户的数学表达式
  6. 将执行结果打印到屏幕上

检验以下main()实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
38 def main():
39 """Main loop: Read and evaluate user's input."""
40 print(WELCOME)
41 while True:
42 # Read user's input
43 try:
44 expression = input(f"{PS1} ")
45 except (KeyboardInterrupt, EOFError):
46 raise SystemExit()
47
48 # Handle special commands
49 if expression.lower() == "help":
50 print(USAGE)
51 continue
52 if expression.lower() in {"quit", "exit"}:
53 raise SystemExit()
54
55 # Evaluate the expression and handle errors
56 try:
57 result = evaluate(expression)
58 except SyntaxError:
59 # If the user enters an invalid expression
60 print("Invalid input expression syntax")
61 continue
62 except (NameError, ValueError) as err:
63 # If the user tries to use a name that isn't allowed
64 # or an invalid value for a given math function
65 print(err)
66 continue
67
68 # Print the result if no error occurs
69 print(f"The result is: {result}")
70
71 if __name__ == "__main__":
72 main()

main()内部,首先打印 WELCOME 消息。然后,你会在try语句中读取用户的输入,以捕获KeyboardInterruptEOFError

如果这些异常之一发生,则终止应用程序。如果用户输入 help 选项,则应用程序将显示你的 USAGE
同样,如果用户输入 quitexit,则应用程序终止。

最后,你使用 evaluate() 计算用户的数学表达式,然后将结果打印到屏幕上。请务必注意,对evaluate()的调用会引发以下异常:

SyntaxError:当用户输入不遵循 Python 语法的表达式时,就会发生这种情况。

NameError:当用户尝试使用不允许的名称(函数,类或属性)时,会发生这种情况。

ValueError:当用户尝试使用不允许在 math 中给定函数输入的值时,会发生这种情况。`

注意:main() 中,你捕获了所有这些异常并相应地向用户打印消息。这将使用户可以查看表达式,解决问题并再次运行程序。而已!

你已经使用Python的 eval() 在大约70行代码中构建了一个数学表达式执行器。要运行应用程序,请打开系统的命令行,然后输入以下命令:

1
$ python3 mathrepl.py

此命令将启动数学表达式执行器的命令行界面(CLI)。你会在屏幕上看到以下内容:

1
2
3
4
5
6
MathREPL 1.0, your Python math expressions evaluator!
Enter a valid math expression after the prompt "mr>>".
Type "help" for more information.
Type "quit" or "exit" to exit.

mr>>

在那里,你可以输入和执行任何数学表达式。例如,键入以下表达式:

1
2
3
4
5
6
mr>> 25 * 2
The result is: 50
mr>> sqrt(25)
The result is: 5.0
mr>> pi
The result is: 3.141592653589793

如果输入有效的数学表达式,则应用程序将对其求值并将结果打印到屏幕上。如果你的表达式有任何问题,那么应用程序将告诉你:

1
2
3
4
5
6
7
8
mr>> 5 * (25 + 4
Invalid input expression syntax
mr>> sum([1, 2, 3, 4, 5])
The use of 'sum' is not allowed
mr>> sqrt(-15)
math domain error
mr>> factorial(-15)
factorial() not defined for negative values

在第一个示例中,你错过了右括号,因此你收到一条消息,告诉你语法不正确。然后,调用 sum() (这是不允许的),并得到一条说明性错误消息。最后,你使用无效的输入值调用“数学”函数,应用程序会生成一条消息,指出你输入中的问题。

你的数学表达式执行器已经准备就绪!你可以随时添加一些额外的功能。

Conclusion

你可以使用 Python 的 eval() 来执行基于字符串或基于代码的输入中的 Python 表达式。当你尝试动态执行Python 表达式并且希望避免从头开始创建自己的表达式计算器时,此内置函数会很有用。

在本教程中,你学习了 eval() 的工作方式以及如何安全有效地使用它来执行任意 Python 表达式。

你现在能够:

使用Python的eval()动态执行基本的Python表达式

运行更复杂的语句,例如函数调用对象创建和使用 eval() 进行属性访问

最大限度地减少与使用Python的 eval() 相关的安全风险

此外,你还编写了一个使用eval()进行交互执行的应用程序使用命令行界面的数学表达式。

使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

扫描二维码,分享此文章