本文是一篇关于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()
也接受两个可选参数:
- globals
- locals
在接下来的三个部分,你会了解到这是哪个参数是什么以及eval()
如何使用它们去动态执行 Python 表达式。
Note: 你也可以使用 exec()
去动态的执行 Python 的代码。eval()
和 exec()
最主要的不同是 eval()
仅可以执行 Python 的表达式,而 exec()
可以执行任意的 Python 代码。
The First Argument: expression
eval()
的第一个参数被称为 expression. 它是必选参数,保存着传递给函数的基于字符串或者编译代码的输入。当你调用 eval()
时,expression 的内容作为 Python 表达式被执行。查看下面基于字符串输入的例子:
1 | "2 ** 8") eval( |
当你使用字符串作为参数调用 eval()
的时候,函数的返回值是执行输入字符串得到的结果。因此,eval()
可以得到全局变量的值,比如上面例子中的 x
。
为了执行一个基于字符串的表达式,Python 的 eval() 会以下面的步骤运行:
解析表达式
- 将它编译为字节码
- 作为Python表达式进行执行
- 返回执行结果
变量 expression 作为 eval()
的第一个参数突出表达了函数仅和表达式一起工作而不是复合语句。Python文档中关于expression的定义如下:
expression
可以求出某个值的语法单元。 换句话说,一个表达式就是表达元素例如字面值、名称、属性访问、运算符或函数调用的汇总,它们最终都会返回一个值。 与许多其他语言不同,并非所有语言构件都是表达式。 还存在不能被用作表达式的 statement,例如
while
。 赋值也是属于语句而非表达式。 (Source)
另一方面,一个Python的语句有如下的定义:
statement
语句是程序段(一个代码“块”)的组成单位。一条语句可以是一个 expression 或某个带有关键字的结构,例如
if
、while
或for
。 (Source)
如果你尝试传递一个复合语句给 eval()
, 然后就会产生一个 SyntaxError
。查看下面的例子,在这个例子中你尝试使用 eval()
执行一个 if 语句。
1 | 100 x = |
如果你尝试使用 Python 的 eval()
方法执行一个复合语句,那么你会得到一个像上面的 trackback 一样的 SyntaxError
。这是因为 eval()
仅仅只接受表达式(expressions)。任何其它的诸如 if
, for
, while
, import
, def
, 或者 class
的语句, 都会产生一个error
。
Note: 一个 for
循环是一个复合语句,但是 for
关键字可以被用在推导式 ( comprehensions ) 中,这被认为是一个表达式。你可以使用 eval()
去执行推导式 ( comprehensions ) 即使它使用了 for
关键字。
赋值操作也同样不被允许用在 eval()
中。
1 | "pi = 3.1416") eval( |
如果你尝试将赋值操作作为参数传递给 Python 的 eval()
函数,那么你会得到一个 SynctaxError
。赋值操作是一个语句而不是表达式,并且语句是不被 eval()
允许的参数类型。
当解析器无法理解你输入的表达式时,你同样会得到一个 SynctaxError
。接下来的例子中,尝试执行了一个违反 Python 语法的表达式。
1 | # Incomplete expression |
你不能传递一个违反Python语法的表达式给 eval()
。在上面的例子中,你尝试执行一个不完整的表达式 ( “5 + 7 *” )
然后你得到了一个 SynctaxError
,因为解析器不理解这个表达式的语法。
你同样可以传递一个编译过的代码对象给 Python 的 eval()
方法。为了编译能够传递给 eval()
的代码,你可以使用 compile()
函数。这是一个内置的函数,它可以将一个输入的字符串编译为代码对象或者一个抽象语法树对象( AST object) ,因此你可以用 eval()
方法执行编译过的代码。
如何使用compile() 方法的细节超过了这篇文章的范围,但是这里速览一下它的前三个必备参数:
- source 保存了你想编译的源代码。这个参数接收通常的字符串,字节字符以及 AST对象。
- filename 给了读取代码的文件。如果你想使用一个基于字符串的输入,那么这个参数的值应该为
“”
。 - mode 具体指明了你想得到什么类型的编译代码。如果你想使用 eval() 处理编译后的代码,那么这个参数需要被设置为
“eval”
。
Note: 想获取更多关于 compile()
的信息,可以查看官方文档。
你可以使用 compile()
将代码对象提供给 eval()
,而不是提供普通字符串。查看以下示例:
1 | # Arithmetic operations |
如果你使用compile() 来编译要传递给eval() 的表达式,则eval() 会经历以下步骤:
- 执行编译后的代码
- 返回执行结果
如果你使用基于编译代码的输入调用 Python 的eval()
,则该函数将进行执行步骤并立即返回结果。当你需要多次执行同一个表达式时,这会十分方便。在这种情况下,最好预先编译表达式,并在随后对 eval()
的调用中重用结果字节码。
如果你预先编译输入表达式,则对 eval()
的后续调用将运行得更快,因为你将不会重复解析和编译步骤。如果你要执行复杂的表达式,不必要的重复会导致 CPU 时间增加以及过多的内存消耗。
The Second Argument: globals
eval()
的第二个参数被称为 globals
。它是一个可选参数,保存着一个字典类型。它为 eval()
函数提供一个全局的命名空间。有了 globals
,你可以告诉 eval()
当执行 expression
时,哪个全局变量需要使用。
全局变量就是那些可以在你当前全局作用域或者命名空间中可以获得的变量。你可以在你代码的任意地方访问这些变量。
所有这些变量在一个字典中被传递给 globals
,当 eval()
执行的时候,可以获取到这些全局的变量。查看下面的例子,它展示了如何使用一个普通的字典去为 eval()
提供一个全局的命名空间。
1 | 100 # A global variable x = |
如果你为 eval()
的参数 globals
提供一个普通的字典,那么eval()
将只采用这个字典中的变量作为全局变量。任何其他定义在这个字典之外的全局变量都不能被 eval()
内部获得。这就是为什么当你在上面代码中尝试获取 y
时,Python 引发了一个 NameError
:传递给 globals
的参数不包含 y
。
你可以通过在你的字典中列举这些变量,然后向 globals 中插入这些变量,然后这些变量在执行期间就可以获取了。例如,如果你将 y
插入 globals
,那么上面例子中 “x + y”
的执行就会如预期的那样。
1 | "x + y", {"x": x, "y": y}) eval( |
一旦你把 y
加到 globals
的字典中,那么 "x + y"
的执行结果就会成功并且返回你预期中的值 300。
你也可以提供在你全局作用域中不存在的变量。因此你需要为每个变量提供一个具体的值。eval()
在执行的时候会当做全局变量来解释这些变量。
1 | "x + y + z", {"x": x, "y": y, "z": 300}) eval( |
即使你未在当前的全局作用域中定义 z
,但该变量也存在于 globals
中,它的值为 300。在这种情况下,eval()
可以像访问全局变量一样访问 z
。
globals
背后的机制非常灵活。你可以将任何可见变量 ( 全局,局部或非局部) 传递给全局变量。你也可以在上面的示例中传递自定义键值对,例如 "z":300
。 eval()
会将它们全部视为全局变量。
关于全局变量重要的一点是,如果为其提供的自定义字典中不包含键”__builtins__
“的值,则在解析表达式之前,将自动在 “__builtins__
“ 下插入对内置字典的引用。这样可以确保 eval()
在执行表达式时将完全访问所有 Python 的内置变量。
以下示例显示,即使你为全局变量提供了一个空字典,对 eval()
的调用仍然可以访问 Python 的内置变量:
1 | "sum([2, 2, 2])", {}) eval( |
在上面的代码中,你为 globals 提供了一个空字典({}
)。因此字典中不包含名称为 “__builtins__
“ 的键,Python自动插入了一个 builtins
的引用。因此,当 eval()
解析 expression
时,它能够获取所有的Python的内置变量。
如果当你调用 eval()
时,没有传递一个字典给 globals
参数 ,那么该参数将默认为在调用 eval()
的环境中由globals()
返回的字典。
1 | 100 # A global variable x = |
当你在不提供 globals
参数的情况下调用 eval()
时,该函数将使用 globals()
返回的字典作为其全局命名空间来执行 expression
。因此,在上面的示例中,你可以自由访问x
和 y
,因为它们是当前global作用域。
The Third Argument: locals
Python 的 eval()
函数接收的第三个参数称为 locals
。这是另一个可选参数,它保存着一个字典。在这种情况下,字典保包含 eval()
执行 expression
时当做局部变量的变量。
局部变量就是那些你定义在一个给定函数内部的变量 (variables, functions, classes, and so on)。局部变量仅在函数内部可见。当你写一个函数的时候会定义这些类型的变量。
一旦 编写了 eval()
,你不能向代码和局部作用域中添加局部变量。然而你可以传递一个字典给 locals
, 然后 eval()
会把这些变量作为局部变量。
1 | "x + 100", {}, {"x": 100}) eval( |
第二个字典在第一次调用 eval()
时会保存变量 x
。这个变量被 eval()
解释为一个局部变量。从另一方面来说,它被视为 eval()
主体中定义的变量。
你可以在 expression
中使用 x
,并且 eval()
会接受这个参数。相反的,如果你想使用 y
,那么你会得到一个NameError
,因为 y
并没有在 globals
或者 locals
的命名空间中定义。
就如 globals
一样,你可以传递任何可见的参数 (global, local, 或者 nolocal) 给 locals
。就如上面的例子中一样,你同样可以传递如 “x”: 100
这样的键值对。eval()
会把他们都作为局部变量。
需要注意的是,当你想提供一个字典给 locals
时,你首先需要提供一个字典给 globals
。在eval()
中是无法使用关键字的。
1 | "x + 100", locals={"x": 100}) eval( |
如果你尝试在调用 eval()
时使用关键字参数,那么你会得到一个 TypeError
,告诉你 eval()
不接受关键字参数。因此,你需要去提供一个 globals
参数的字典,然后才能使用 locals
的字典。
如果你不向 locals
提供一个字典,那么它默认指向传递给的字典。下面是一个关于你传递给 globals
一个空字典并且未传任何东西给 locals
的例子:
1 | 100 x = |
从上面可以看出,当你不传任何字典给locals
时,参数默认为传递给 globals
的字典。在这种情矿下,eval()
无法获得 x
的值,因为 globals
字典为空。
globals
和 locals
之间的主要实际区别在于,如果该 globals
中 builtins
键不存在,那么 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 | 100 x = |
你也可以在调用 eval()
过程中使用包含下面这些 Python 操作符的 Boolean 表达式:
- Value comparison operators:
<
,>
,<=
,>=
,==
,!=
- Logical (Boolean) operators:
and
,or
,not
- Membership test operators:
in
,not in
- Identity operators:
is
,is not
在这种情况下,函数返回你执行的表达式的真值结果。
现在,你可能在想,为啥我们要使用eval() 而不是直接使用Boolean表达式?想一下,假设你需要实现一个条件语句,但是你想在执行过程中改变这个条件:
1 | def func(a, b, condition): |
在 func()
中,你使用 eval()
去执行提供的条件,然后决定返回的结果是由 a+b
或者 a-b
中的一个。在上面的示例中,你仅使用了几种不同的条件,但是只要你坚持使用在func() 中定义的名称 a
和 b
,就可以使用任意数量的其他条件。
现在想象一下如何在不使用Python的eval()
的情况下实现类似的功能。这样会减少代码和时间吗?肯定不行的!
Math Expressions
一个最常使用Python eval()
方法的地方就是计算基于字符输入的数学表达式。例如,当你想创造一个python 的计算器时,你就可以使用eval() 去执行用户的输入然后返回计算的结果。
下面就是一个使用 eval()
如何执行数学运算的例子:
1 | # Arithmetic operations |
当你使用 eval()
去执行数学表达式的时候,你可以传递任何复杂程度的表达式。eval()
会解析它们,执行它们,如果一切正常的情况下,会返回你预期的结果。
General-Purpose Expressions
到目前为止,你知道了如何使用eval() 去执行Boolean和数学表达式。然而,你可以使用eval() 去执行更加复杂的Python表达式,包括函数调用,对象创建,属性获取,推导式等等。
例如,你可以调用一个内置函数或者你导入的标准库或者三方模块:
1 | # Run the echo command |
在这个例子中,你使用 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
你可以通过将自定义字典传递给 globals
和 locals
参数限制 eval()
执行的环境。例如,你可以传递空字典给两个参数以防止eval() 访问调用者当前的作用域或者命名空间。
1 | # Avoid access to names in the caller's current scope |
如果你向 globals
和 locals
传递空字典 ({}
),那么 eval()
在执行字符串 x * 5
时不会在全局命名空间或者它的局部命名空间中找到变量 x
。作为结果,eval()
会抛出一个 NameError
。
不幸的是,像这样限制 globals
和 locals
参数并不能消除所有使用Python的 eval()
相关的所有安全风险,因为你仍然可以访问所有Python的内置变量。
Restricting the Use of Built-In Names
如前所述,Python的eval()
在解析 expression
之前会自动将对 builtins
字典的引用插入 globals
中。恶意用户可以通过使用内置函数 __import__()
来利用此行为,以访问标准库和系统上已安装的任何第三方模块。
下面例子说明,即使你已限制 globals
和 locals
,也可以使用任何内置函数和任何标准模块(如 math
或 subprocess
):
1 | "sum([5, 5, 5])", {}, {}) eval( |
即使你使用空字典限制了globals
和locals
,但是你仍然可以使用任何内置函数,就像在上面的代码中对sum()
和__import__()
所做的一样。
你可以使用__import__()
来导入任何标准或第三方模块,就像上面对math
和subprocess
所做的一样。使用这种技术,你可以访问在 math
,subprocess
或任何其他模块中定义的任何函数或类。现在想象一下,恶意用户可以使用 subprocess
或标准库中的任何其他功能强大的模块对你的系统进行操作。
为了最大程度地降低这种风险,你可以通过覆盖 globals
中的 __builtins__
键来限制对Python内置函数的访问。较好的做法建议使用包含键值对 "__builtins__": {}
的自定义词典。查看下面的例子:
1 | "__import__('math').sqrt(25)", {"__builtins__": {}}, {}) eval( |
如果你将包含键值对"__builtins__": {}
的字典传递给globals
,则eval()
将无法直接访问Python的内置函数,例如__import__()
。但是,正如你将在下一节中看到的那样,这种方法仍无法完全使 eval()
安全。
Restricting Names in the Input
即使你可以使用自定义全局变量和局部变量字典来限制Python的 eval()
的执行环境,该函数仍然容易受到一些特殊技巧的攻击。例如,你可以使用类型字符如“”,[],{}或 () 以及一些特殊属性来访问类对象:
1 | "".__class__.__base__ |
一旦你访问了object
后,你就可以使用特殊方法.__ subclasses__()
来访问所有继承自object的类。方式如下:
1 | >>> for sub_class in ().__class__.__base__.__subclasses__(): |
此代码会将大量的类列表打印到屏幕上。其中一些类非常强大,如果使用不当,可能会非常危险。这就打开了另一个重要的安全漏洞,你只是简单地限制eval()
的执行环境是无法解决该漏洞的:
1 | """[ input_string = |
上面代码中的列表推导式会过滤从 object
对象继承的类,返回包含 类 range
的 list
。第一个索引([0]
)返回类 rassssnge
。一旦可以访问 range
,就可以调用它来生成 range
对象。然后,你在 range
对象上调用list()
以生成十个整数的列表。
在这个例子中,使用的range
方法来说明 eval()
中的安全漏洞。想象一下,如果你的系统暴露了subprocess.Popen
之类的类,恶意用户会做什么。
*Note: * 想要深入了解 eval()
方法的漏洞,请查看 Ned Batchelder 的文章:Eval确实很危险。该漏洞的一种可能解决方案是将输入中变量的使用限制为 safe名称或这没有任何变量。要实现此方法,你需要执行以下步骤:
- 创建包含要与
eval()
一起使用的名称的字典。 - 在模式 “eval” 下,使用
compile()
将输入字符串编译为字节码。 - 检查字节码对象上的
.co_names
以确保它仅包含允许的变量。 - 如果用户尝试输入不允许的变量,请引发一个
NameError
。
看一下实现所有这些步骤的函数:
1 | def eval_expression(input_string): |
在eval_expression()
中,你实现了之前看到的所有步骤。该函数将可以与eval()
一起使用的变量限制为仅字典allowed_names
中的那些变量。为此,该函数使用.co_names
,它是代码对象的一个属性,返回包含代码对象中的名称的元组。
以下示例显示了eval_expression()
在实际中的工作方式:
1 | "3 + 4 * 5 + 25 / 2") eval_expression( |
如果你调用 eval_expression()
来执行算术运算,或者使用包含允许名称的表达式,则将获得预期的结果。否则,你会收到 NameError
。在上述示例中,你唯一允许使用的名称是sum()
。不允许使用诸如 len()
和 pow()
之类的其他名称,因此当你尝试使用它们时,该函数会引发NameError
。
如果你想完全禁止使用名称,则可以如下重写eval_expression()
:
1 | def eval_expression(input_string): |
现在,你的函数不允许输入字符串中的任何名称。要做到这一点,你需要检查 .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 | from ast import literal_eval |
注意,literal_eval()
仅适用于标准类型文字。它不支持使用运算符或变量。如果你尝试将表达式提供给 literal_eval()
,则会收到 ValueError
。此函数还可以帮助你将与使用Python的 eval()
相关的安全风险降至最低。
Using Python’s eval()
With input()
在Python 3.x中,内置的input()
在命令行中读取用户输入,将其转换为字符串,剥离尾部的换行符,然后将结果返回给调用方。由于 input()
的结果是一个字符串,因此你可以将其输入到 eval()
并将其作为Python表达式求值:
1 | "Enter a math expression: ")) eval(input( |
你可以将 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()
的情况下解决此问题,则需要执行以下步骤:
- 解析输入表达式。
- 将表达式的组件更改为Python对象(数字,运算符,函数等)。
- 将所有内容组合成一个表达式。
- 确认该表达式在Python中有效。
- 执行最终表达式并返回结果。
考虑到Python可以处理和执行的各种可能的表达式,这会花费大量的时间。幸运的是,你可以使用 eval()
解决此问题,并且你已经学习了几种降低相关安全风险的方法。
首先,创建一个名为 mathrepl.py
的 Python 脚本,然后添加以下代码:
1 | 1 import math |
在这段代码中,你首先导入 Python 的 math
模块。该模块将允许你使用预定义的函数和常量执行数学运算。常量 ALLOWED_NAMES
保存一个字典,其中包含 math
中的非特殊名称。这样,你就可以将它们与eval()
一起使用。
你还定义了三个字符串常量。你将使用它们作为脚本的用户界面,并根据需要将它们打印到屏幕上。
现在,你可以编写应用程序的核心功能了。在这种情况下,你需要编写一个函数来接收数学表达式作为输入并返回其结果。为此,你编写了一个名为evaluate()
的函数:
1 | 26 def evaluate(expression): |
该函数的工作方式如下:
- 在第26行中,定义
evaluate()
。该函数以字符串expression
作为参数,并返回一个float,该值表示将字符串作为数学表达式求值的结果。 - 在第29行中,使用
compile()
将输入字符串expression
转换为已编译的 Python 代码。如果用户输入无效的表达式,则编译操作将引发SyntaxError
。 - 在第32行中,启动
for
循环以检查expression
中包含的名称,并确认可以在最终表达式中使用它们。如果用户提供的名称不在允许的名称列表中,则引发NameError
。 - 在第36行中,对数学表达式进行实际执行。注意,按照惯例,你将自定义字典传递给
globals
和locals
。ALLOWED_NAMES
保存在math
中定义的函数和常量。
注意:由于此应用程序使用了math
中定义的函数,因此你需要考虑到当使用无效输入值调用它们时,其中一些函数会引发ValueError
。
例如,math.sqrt(-10)会引发错误,因为-10的平方根 未定义。
稍后,你将在客户端代码中看到如何捕获此错误。对globals
和locals
参数使用自定义值,以及对line33
中的名称进行检查,可以使与eval()
相关的安全风险降至最低。
当你在main()
中编写其客户端代码时,你的数学表达式执行器将要完成。在此函数中,你将定义程序的主循环,并关闭读取和执行用户在命令行中输入的表达式的周期。
对于此示例,应用程序将:
- 向用户打印欢迎消息
- 显示提示,准备读取用户的输入
- 提供选项以获取使用说明并终止应用程序
- 读取用户的数学表达式
- 执行用户的数学表达式
- 将执行结果打印到屏幕上
检验以下main()
实现:
1 | 38 def main(): |
在main()
内部,首先打印 WELCOME
消息。然后,你会在try
语句中读取用户的输入,以捕获KeyboardInterrupt
和 EOFError
。
如果这些异常之一发生,则终止应用程序。如果用户输入 help
选项,则应用程序将显示你的 USAGE
。
同样,如果用户输入 quit
或 exit
,则应用程序终止。
最后,你使用 evaluate()
计算用户的数学表达式,然后将结果打印到屏幕上。请务必注意,对evaluate()
的调用会引发以下异常:
SyntaxError:当用户输入不遵循 Python 语法的表达式时,就会发生这种情况。
NameError:当用户尝试使用不允许的名称(函数,类或属性)时,会发生这种情况。
ValueError:当用户尝试使用不允许在 math
中给定函数输入的值时,会发生这种情况。`
注意: 在 main()
中,你捕获了所有这些异常并相应地向用户打印消息。这将使用户可以查看表达式,解决问题并再次运行程序。而已!
你已经使用Python的 eval()
在大约70行代码中构建了一个数学表达式执行器。要运行应用程序,请打开系统的命令行,然后输入以下命令:
1 | python3 mathrepl.py |
此命令将启动数学表达式执行器的命令行界面(CLI)。你会在屏幕上看到以下内容:
1 | MathREPL 1.0, your Python math expressions evaluator! |
在那里,你可以输入和执行任何数学表达式。例如,键入以下表达式:
1 | mr>> 25 * 2 |
如果输入有效的数学表达式,则应用程序将对其求值并将结果打印到屏幕上。如果你的表达式有任何问题,那么应用程序将告诉你:
1 | mr>> 5 * (25 + 4 |
在第一个示例中,你错过了右括号,因此你收到一条消息,告诉你语法不正确。然后,调用 sum()
(这是不允许的),并得到一条说明性错误消息。最后,你使用无效的输入值调用“数学”函数,应用程序会生成一条消息,指出你输入中的问题。
你的数学表达式执行器已经准备就绪!你可以随时添加一些额外的功能。
Conclusion
你可以使用 Python 的 eval()
来执行基于字符串或基于代码的输入中的 Python 表达式。当你尝试动态执行Python 表达式并且希望避免从头开始创建自己的表达式计算器时,此内置函数会很有用。
在本教程中,你学习了 eval()
的工作方式以及如何安全有效地使用它来执行任意 Python 表达式。
你现在能够:
使用Python的eval()
动态执行基本的Python表达式
运行更复杂的语句,例如函数调用,对象创建和使用 eval()
进行属性访问
最大限度地减少与使用Python的 eval()
相关的安全风险
此外,你还编写了一个使用eval()
进行交互执行的应用程序使用命令行界面的数学表达式。
若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏
扫描二维码,分享此文章