Python eval():动态评估表达式
目录
Pythoneval()
允许您从基于字符串或基于编译代码的输入中评估任意 Python表达式。当您尝试从作为字符串或编译代码对象出现的任何输入动态评估 Python 表达式时,此函数会很方便。
尽管 Pythoneval()
是一个非常有用的工具,但该函数具有一些重要的安全隐患,您在使用它之前应该考虑这些问题。在本教程中,您将了解它的eval()
工作原理以及如何在 Python 程序中安全有效地使用它。
在本教程中,您将学习:
- Python 的
eval()
工作原理 - 如何使用
eval()
,以动态评估任意基于字符串或基于编译的代码输入 - 如何
eval()
使您的代码不安全以及如何最大程度地减少相关的安全风险
此外,您将学习如何使用 Pythoneval()
编写交互式计算数学表达式的应用程序。通过此示例,您将把学到的所有知识应用eval()
到实际问题中。如果您想获取此应用程序的代码,则可以单击下面的框:
理解 Python 的 eval()
您可以使用内置 Pythoneval()
从基于字符串或基于编译代码的输入动态计算表达式。如果您将字符串传递给eval()
,则该函数会解析它,将其编译为bytecode,并将其作为 Python 表达式进行计算。但是,如果您eval()
使用编译后的代码对象调用,则该函数仅执行求值步骤,如果您eval()
使用相同的输入多次调用,这将非常方便。
Python 的签名eval()
定义如下:
eval(expression[, globals[, locals]])
该函数采用第一个参数,称为expression
,它保存您需要计算的表达式。eval()
还需要两个可选参数:
globals
locals
在接下来的三个部分中,您将了解这些参数是什么以及如何eval()
使用它们即时评估 Python 表达式。
注意:您还可以使用exec()
动态执行 Python 代码。eval()
和之间的主要区别exec()
是eval()
只能执行或计算表达式,而exec()
可以执行任何一段 Python 代码。
第一个论点: expression
的第一个参数eval()
称为expression
。它是一个必需的参数,用于保存函数的基于字符串或基于编译代码的输入。当您调用 时eval()
, 的内容expression
被评估为 Python 表达式。查看以下使用基于字符串的输入的示例:
>>> eval("2 ** 8")
256
>>> eval("1024 + 1024")
2048
>>> eval("sum([8, 16, 32])")
56
>>> x = 100
>>> eval("x * 2")
200
当您eval()
使用字符串作为参数调用时,该函数将返回对输入字符串求值的结果。默认情况下,eval()
可以访问x
上面示例中的全局名称。
要评估基于字符串的expression
,Pythoneval()
运行以下步骤:
- 解析
expression
- 编译成字节码
- 将其作为 Python 表达式进行评估
- 返回评估结果
第expression
一个参数的名称eval()
强调了该函数仅适用于表达式而不适用于复合语句。的Python文档定义表达式如下:
表达
可以评估为某个值的一段语法。换句话说,表达式是表达式元素的累积,如文字、名称、属性访问、运算符或函数调用,它们都返回一个值。与许多其他语言相比,并非所有语言结构都是表达式。还有一些不能用作表达式的语句,例如
while
. 赋值也是语句,而不是表达式。(来源)
另一方面,Python语句具有以下定义:
陈述
语句是套件(代码“块”)的一部分。语句是一个表达式或几个带有关键字的结构之一,例如
if
,while
或for
。(来源)
如果您尝试将复合语句传递给eval()
,那么您将得到一个SyntaxError
. 看看下面的例子中,你试图执行的if
语句使用eval()
:
>>> x = 100
>>> eval("if x: print(x)")
File "<string>", line 1
if x: print(x)
^
SyntaxError: invalid syntax
如果您尝试使用 Python 的 评估复合语句eval()
,那么您将SyntaxError
在上面的traceback 中得到类似的结果。那是因为eval()
只接受表达式。任何其他语句,例如if
, for
, while
, import
, def
, or class
,都会引发错误。
赋值操作不允许用于eval()
:
>>> eval("pi = 3.1416")
File "<string>", line 1
pi = 3.1416
^
SyntaxError: invalid syntax
如果您尝试将赋值操作作为参数传递给 Python 的eval()
,那么您将得到一个SyntaxError
. 赋值操作是语句而不是表达式,并且语句不允许与eval()
.
SyntaxError
每当解析器不理解输入表达式时,您也会得到一个。请看下面的示例,在该示例中,您尝试评估违反 Python 语法的表达式:
>>> # Incomplete expression
>>> eval("5 + 7 *")
File "<string>", line 1
5 + 7 *
^
SyntaxError: unexpected EOF while parsing
你不能传递一个eval()
违反 Python 语法的表达式。在上面的示例中,您尝试计算一个不完整的表达式 ( "5 + 7 *"
) 并得到 a,SyntaxError
因为解析器不理解该表达式的语法。
您还可以将编译后的代码对象传递给 Python 的eval()
. 要编译您要传递给 的代码eval()
,您可以使用compile()
. 这是一个内置函数,可以将输入字符串编译为代码对象或AST 对象,以便您可以使用eval()
.
如何使用的细节compile()
超出了本教程的范围,但这里快速浏览一下它的前三个必需参数:
source
保存要编译的源代码。此参数接受普通字符串、字节字符串和 AST 对象。filename
给出从中读取代码的文件。如果要使用基于字符串的输入,则此参数的值应为"<string>"
.mode
指定要获得哪种编译代码。如果要使用 处理编译后的代码eval()
,则应将此参数设置为"eval"
。
注意:有关 的更多信息compile()
,请查看官方文档。
您可以使用compile()
代码对象来eval()
代替普通字符串。查看以下示例:
>>> # 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()
执行以下步骤:
- 评估编译的代码
- 返回评估结果
如果您eval()
使用基于编译代码的输入调用 Python ,则该函数执行评估步骤并立即返回结果。当您需要多次评估同一个表达式时,这会很方便。在这种情况下,最好预编译表达式并在后续调用eval()
.
如果您事先编译输入表达式,那么连续调用eval()
将运行得更快,因为您不会重复解析和编译步骤。如果您正在评估复杂的表达式,不必要的重复会导致高 CPU 时间和过多的内存消耗。
第二个论点: globals
的第二个参数eval()
称为globals
。它是可选的,并拥有一个字典,提供了一个全球命名空间来eval()
。使用globals
,您可以判断eval()
在评估时使用哪些全局名称expression
。
全局名称是在您当前的全局范围或命名空间中可用的所有名称。您可以从代码中的任何位置访问它们。
传递给globals
字典的所有名称eval()
在执行时都可用。看看下面的例子,用它展示了如何提供一个全球性的自定义词典的命名空间来eval()
:
>>> 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
如果您为 的globals
参数提供自定义字典eval()
,eval()
则将仅将这些名称作为全局变量。无法从内部访问在此自定义字典之外定义的任何全局名称eval()
。这就是为什么NameError
当您尝试y
在上面的代码中访问时Python 引发 a 的原因:传递给的字典globals
不包含y
.
您可以globals
通过在字典中列出名称来插入名称,然后这些名称将在评估过程中可用。例如,如果您插入y
到globals
,则"x + y"
上面示例中的 评估将按预期工作:
>>> eval("x + y", {"x": x, "y": y})
300
由于您添加y
到您的自定义globals
字典, 的评估"x + y"
成功并且您获得 的预期返回值300
。
您还可以提供当前全局范围内不存在的名称。为此,您需要为每个名称提供一个具体的值。eval()
运行时将这些名称解释为全局名称:
>>> 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
相当灵活。您可以将任何可见变量(全局、本地或非本地)传递给globals
. 您还可以像"z": 300
上面的示例一样传递自定义键值对。eval()
将所有这些都视为全局变量。
重要的一点globals
是,如果您向其提供不包含 key 值的自定义字典"__builtins__"
,则在解析之前builtins
将自动插入对字典的引用。这确保在评估."__builtins__"
expression
eval()
expression
以下示例表明,即使您向 提供了一个空字典globals
,对 的调用eval()
仍然可以访问 Python 的内置名称:
>>> eval("sum([2, 2, 2])", {})
6
>>> eval("min([1, 2, 3])", {})
1
>>> eval("pow(10, 2)", {})
100
在上面的代码中,您向 提供了一个空字典 ( {}
) globals
。由于该字典不包含名为 的键"__builtins__"
,Python 会自动插入一个引用builtins
. 这样,eval()
在解析expression
.
如果调用时eval()
未将自定义字典传递给globals
,则参数将默认为在调用globals()
的环境中返回的字典eval()
:
>>> x = 100 # A global variable
>>> y = 200 # Another global variable
>>> eval("x + y") # Access both global variables
300
当您在eval()
不提供globals
参数的情况下调用时,该函数expression
使用返回的字典globals()
作为其全局命名空间进行计算。因此,在上面的示例中,您可以自由访问x
并且y
因为它们是包含在当前全局范围内的全局变量。
第三个论点: locals
Python 的eval()
第三个参数叫做locals
. 这是保存字典的另一个可选参数。在这种情况下,字典包含eval()
在评估 时用作本地名称的变量expression
。
本地名称是您在给定函数内定义的那些名称(变量、函数、类等)。局部名称仅在封闭函数内部可见。您在编写函数时定义这些类型的名称。
由于eval()
已经写入,您不能将本地名称添加到其 code 或local scope。但是,您可以将字典传递给locals
,并将eval()
这些名称视为本地名称:
>>> 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()
.
您可以使用x
in expression
,并且eval()
可以访问它。相反,如果您尝试使用y
,那么您将得到 aNameError
因为y
未在globals
名称空间或locals
名称空间中定义。
与 一样globals
,您可以将任何可见变量(全局、本地或非本地)传递给locals
。您还可以像"x": 100
上面的示例一样传递自定义键值对。eval()
将所有这些都视为局部变量。
请注意,要向 提供字典locals
,您首先需要向 提供字典globals
。不能将关键字参数用于eval()
:
>>> 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
。这是一个示例,其中您将空字典传递给 ,globals
而没有任何内容locals
:
>>> 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
持有空字典。
globals
和之间的主要实际区别在于,如果该键不存在locals
,Python 将自动插入一个"__builtins__"
键globals
。无论您是否向 提供自定义字典,都会发生这种情况globals
。另一方面,如果您向 提供自定义字典locals
,则该字典在 执行期间将保持不变eval()
。
使用 Python 评估表达式 eval()
您可以使用 Pythoneval()
来计算任何类型的 Python 表达式,但不能计算 Python 语句,例如基于关键字的复合语句或赋值语句。
eval()
当您需要动态评估表达式并且使用其他 Python 技术或工具会显着增加您的开发时间和精力时,这会很方便。在本节中,您将学习如何使用 Pythoneval()
来计算布尔值、数学和通用 Python 表达式。
布尔表达式
布尔表达式是 Python 表达式,当解释器计算它们时返回真值(True
或False
)。它们通常用于if
语句中以检查某些条件是真还是假。由于布尔表达式不是复合语句,您可以使用eval()
它们来计算:
>>> 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 运算符的布尔表达式一起使用:
在所有情况下,该函数都会返回您正在评估的表达式的真值。
现在,您可能在想,为什么我要使用eval()
而不是直接使用布尔表达式?好吧,假设您需要实现一个条件语句,但您想即时更改条件:
>>> 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()
评估所提供的内容condition
并根据评估结果a + b
或或a - b
根据评估结果返回。在上面的示例中,您只使用了几个不同的条件,但您可以使用任意数量的其他条件,前提是您坚持使用名称a
并b
在func()
.
现在想象一下如何在不使用 Python 的eval()
. 那会花费更少的代码和时间吗?没门!
数学表达式
Python 的一个常见用例eval()
是从基于字符串的输入中计算数学表达式。例如,如果您想创建一个Python 计算器,那么您可以使用它eval()
来评估用户的输入并返回计算结果。
以下示例展示了如何使用eval()
withmath
来执行数学运算:
>>> # 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()
将解析它们,评估它们,如果一切正常,会给你预期的结果。
通用表达式
到目前为止,您已经学习了如何使用eval()
布尔表达式和数学表达式。但是,您可以使用eval()
包含函数调用、对象创建、属性访问、推导等的更复杂的 Python 表达式。
例如,您可以调用内置函数或使用标准或第三方模块导入的函数:
>>> # Run the echo command
>>> import subprocess
>>> eval("subprocess.getoutput('echo Hello, World')")
'Hello, World'
>>> # Launch Firefox (if available)
>>> eval("subprocess.getoutput('firefox')")
''
在本例中,您使用 Pythoneval()
来执行一些系统命令。你可以想像,你可以做吨的有用的东西有此功能。但是,这eval()
也可能使您面临严重的安全风险,例如允许恶意用户在您的机器中运行系统命令或任意代码段。
在下一节中,您将了解解决与 eval() 相关的一些安全风险的方法。
最大限度地减少安全问题 eval()
尽管 Python 的用途几乎是无限的,但它eval()
也具有重要的安全隐患。eval()
被认为是不安全的,因为它允许您(或您的用户)动态执行任意 Python 代码。
这被认为是不好的编程习惯,因为你在读(或写)的代码是不是,你会执行代码。如果您打算使用eval()
评估来自用户或任何其他外部来源的输入,那么您将无法确定将执行哪些代码。如果您的应用程序在错误的人手中运行,这将是一个严重的安全风险。
出于这个原因,良好的编程实践通常建议不要使用eval()
. 但是,如果您无论如何都选择使用该功能,那么经验法则是永远不要将它与不受信任的输入一起使用。此规则的棘手部分是弄清楚您可以信任哪些类型的输入。
作为eval()
不负责任地使用如何使您的代码不安全的示例,假设您想构建一个用于评估任意 Python 表达式的在线服务。您的用户将介绍表达式,然后单击Run
按钮。应用程序将获取用户的输入并将其传递给eval()
评估。
此应用程序将在您的个人服务器上运行。是的,您拥有所有这些有价值的文件的同一台服务器。如果您正在运行 Linux 机器并且应用程序的进程具有正确的权限,那么恶意用户可能会引入一个危险的字符串,如下所示:
"__import__('subprocess').getoutput('rm –rf *')"
上面的代码将删除应用程序当前目录中的所有文件。那会很糟糕,不是吗?
注意: __import__()
是一个内置函数,它将模块名称作为字符串并返回对模块对象的引用。__import__()
是一个函数,与import
语句完全不同。您不能import
使用eval()
.
当输入不受信任时,没有完全有效的方法来避免与eval()
. 但是,您可以通过限制eval()
. 在以下各节中,您将学习执行此操作的一些技巧。
限制globals
和locals
您可以eval()
通过将自定义字典传递给globals
和locals
参数来限制 的执行环境。例如,您可以将空字典传递给两个参数,以防止eval()
访问调用者当前作用域或命名空间中的名称:
>>> # 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
如果将空字典 ( {}
)传递给globals
and locals
,则在评估 string 时eval()
不会x
在其全局命名空间或本地命名空间中找到该名称"x * 5"
。结果,eval()
会抛出一个NameError
.
不幸的是,像这样限制globals
和locals
参数并不能消除与使用 Python 的 相关的所有安全风险eval()
,因为您仍然可以访问所有 Python 的内置名称。
限制使用内置名称
正如您之前看到的,Python 会在解析之前eval()
自动插入对builtins
into字典的引用。恶意用户可以通过使用内置函数访问标准库和系统上安装的任何第三方模块来利用此行为。globals
expression
__import__()
下面的例子显示,你可以使用任何内置的功能和任何标准的模块状math
或subprocess
甚至您所限制后globals
和locals
:
>>> eval("sum([5, 5, 5])", {}, {})
15
>>> eval("__import__('math').sqrt(25)", {}, {})
5.0
>>> eval("__import__('subprocess').getoutput('echo Hello, World')", {}, {})
'Hello, World'
即使您限制globals
并locals
使用空字典,您仍然可以像在上面的代码中使用sum()
和一样使用任何内置函数__import__()
。
您可以使用__import__()
导入,就像你没有以上任何标准或第三方的模块math
和subprocess
。利用这种技术,你可以访问定义的任何函数或类math
,subprocess
或任何其他模块。现在想象一下恶意用户可以使用subprocess
标准库中的或任何其他强大的模块对您的系统做些什么。
为了尽量减少这种风险,你可以限制Python的内置函数通过重写"__builtins__"
的关键globals
。好的做法建议使用包含键值对的自定义字典"__builtins__": {}
。查看以下示例:
>>> 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()
完全安全。
限制输入中的名称
即使您可以eval()
使用自定义globals
和locals
字典来限制 Python 的执行环境,该函数仍然容易受到一些花哨的技巧的影响。例如,您可以object
使用诸如, ,之类的类型文字或与一些特殊属性一起访问该类:""
[]
{}
()
>>> "".__class__.__base__
<class 'object'>
>>> [].__class__.__base__
<class 'object'>
>>> {}.__class__.__base__
<class 'object'>
>>> ().__class__.__base__
<class 'object'>
一旦您有权访问object
,您就可以使用特殊方法.__subclasses__()
来访问所有继承自 的类object
。这是它的工作原理:
>>> for sub_class in ().__class__.__base__.__subclasses__():
... print(sub_class.__name__)
...
type
weakref
weakcallableproxy
weakproxy
int
...
此代码将在您的屏幕上打印一个大的类列表。其中一些类非常强大,如果落入坏人之手,可能会非常危险。这打开了另一个重要的安全漏洞,您无法通过简单地限制 的执行环境来关闭该漏洞eval()
:
>>> 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
以返回list
包含该类的类range
。第一个索引 ( [0]
) 返回类range
。一旦您可以访问range
,您就可以调用它来生成一个range
对象。然后调用list()
该range
对象以生成一个包含十个整数的列表。
在此示例中,您将使用range
来说明 中的安全漏洞eval()
。现在想象一下,如果您的系统暴露了像subprocess.Popen
.
注意:要深入了解 的漏洞eval()
,请查看 Ned Batchelder 的文章,Eval 真的很危险。
此漏洞的一个可能解决方案是限制输入中名称的使用,要么使用一堆安全名称,要么根本不使用名称。要实现此技术,您需要执行以下步骤:
- 创建一个包含要与 一起使用的名称的字典
eval()
。 - 使用
compile()
in mode将输入字符串编译为字节码"eval"
。 - 检查
.co_names
字节码对象以确保它只包含允许的名称。 NameError
如果用户尝试输入不允许的名称,则提高a 。
看一下您在其中实现所有这些步骤的以下函数:
>>> 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()
在实践中的工作原理:
>>> 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
当您尝试使用它们时,该函数会引发 a 。
如果你想完全禁止使用名称,那么你可以重写eval_expression()
如下:
>>> 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()
并加强您的防御以抵御恶意攻击
将输入限制为仅文字
Python 的一个常见用例eval()
是评估包含标准 Python 文字的字符串并将它们转换为具体对象。
标准库提供了一个literal_eval()
可以帮助实现这一目标的函数。该函数不支持运算符,但支持列表、元组、数字、字符串等:
>>> 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()
.
使用Python的eval()
用input()
在Python 3.x 中,内置input()
函数在命令行读取用户输入,将其转换为字符串,去除尾随的换行符,并将结果返回给调用者。由于结果input()
是一个字符串,您可以将其提供给eval()
Python 表达式并将其计算为:
>>> 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()
因为它模拟了input()
Python 2.x 中的行为,其中input()
将用户的输入计算为 Python 表达式并返回结果。
input()
由于其安全隐患,Python 2.x 中的这种行为在 Python 3.x 中发生了变化。
构建数学表达式评估器
到目前为止,您已经了解了 Python 的eval()
工作原理以及如何在实践中使用它。您还了解到它eval()
具有重要的安全隐患,并且通常认为避免eval()
在代码中使用 的好做法。但是,在某些情况下,Pythoneval()
可以为您节省大量时间和精力。
在本节中,您将编写一个应用程序来动态计算数学表达式。如果您想在不使用 的情况下解决此问题eval()
,则需要执行以下步骤:
- 解析输入表达式。
- 将表达式的组件更改为 Python 对象(数字、运算符、函数等)。
- 将所有内容组合成一个表达式。
- 确认表达式在 Python 中有效。
- 计算最终表达式并返回结果。
考虑到 Python 可以处理和评估的各种可能的表达式,这将是大量的工作。幸运的是,您可以使用它eval()
来解决这个问题,并且您已经学会了几种降低相关安全风险的技术。
您可以通过单击下面的框来获取将在本部分中构建的应用程序的源代码:
首先,启动您最喜欢的代码编辑器。创建一个名为的新Python 脚本mathrepl.py
,然后添加以下代码:
1import math
2
3__version__ = "1.0"
4
5ALLOWED_NAMES = {
6 k: v for k, v in math.__dict__.items() if not k.startswith("__")
7}
8
9PS1 = "mr>>"
10
11WELCOME = f"""
12MathREPL {__version__}, your Python math expressions evaluator!
13Enter a valid math expression after the prompt "{PS1}".
14Type "help" for more information.
15Type "quit" or "exit" to exit.
16"""
17
18USAGE = f"""
19Usage:
20Build math expressions using numeric values and operators.
21Use any of the following functions and constants:
22
23{', '.join(ALLOWED_NAMES.keys())}
24"""
在这段代码中,您首先导入 Python 的math
模块。该模块将允许您使用预定义的函数和常量执行数学运算。该常量ALLOWED_NAMES
包含一个字典,其中包含 中的非特殊名称math
。这样,您就可以将它们与eval()
.
您还定义了另外三个字符串常量。您将使用它们作为脚本的用户界面,并根据需要将它们打印到屏幕上。
现在您已准备好编写应用程序的核心功能。在这种情况下,您希望编写一个函数,该函数接收数学表达式作为输入并返回其结果。为此,您编写了一个名为 的函数evaluate()
:
26def 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)
该函数的工作原理如下:
-
在行
26
,你定义evaluate()
。此函数将字符串expression
作为参数并返回一个浮点数,该浮点数表示将字符串作为数学表达式求值的结果。 -
在行
29
,你用compile()
把输入的字符串expression
为编译的Python代码。SyntaxError
如果用户输入无效表达式,编译操作将引发 a 。 -
在line 中
32
,您启动一个for
循环来检查其中包含的名称expression
并确认它们可以在最终表达式中使用。如果用户提供的名称不在允许的名称列表中,则您会引发NameError
. -
在行
36
中,执行数学表达式的实际评价。请注意,您将自定义词典传递给globals
并locals
按照良好实践的建议。ALLOWED_NAMES
保存中定义的函数和常量math
。
注意:由于此应用程序使用 中定义的函数math
,您需要考虑其中一些函数ValueError
在您使用无效输入值调用它们时会引发 a 。
例如,math.sqrt(-10)
因为会引发错误平方根的-10
不确定。稍后,您将看到如何在客户端代码中捕获此错误。
使用自定义的值globals
和locals
参数,其名称在检查沿着线33
,让您以最小的安全风险与使用相关eval()
。
当您在main()
. 在此函数中,您将定义程序的主循环并关闭读取和评估用户在命令行中输入的表达式的循环。
对于此示例,应用程序将:
- 向用户打印欢迎信息
- 显示准备读取用户输入的提示
- 提供获取使用说明和终止应用程序的选项
- 读取用户的数学表达式
- 评估用户的数学表达式
- 将评估结果打印到屏幕上
查看以下实现main()
:
38def 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
71if __name__ == "__main__":
72 main()
在里面main()
,您首先打印WELCOME
消息。然后您在try
语句中读取用户的输入以捕获KeyboardInterrupt
和EOFError
。如果发生这些异常中的任何一个,则您终止应用程序。
如果用户输入该help
选项,则应用程序会显示您的USAGE
指南。同样,如果用户输入quit
或exit
,则应用程序终止。
最后,您使用evaluate()
评估用户的数学表达式,然后将结果打印到屏幕上。请务必注意,调用 可能evaluate()
会引发以下异常:
SyntaxError
:当用户输入不遵循 Python 语法的表达式时会发生这种情况。NameError
:当用户尝试使用不允许的名称(函数、类或属性)时会发生这种情况。ValueError
:当用户尝试使用不允许的值作为math
.
请注意,在 中main()
,您捕获所有这些异常并相应地向用户打印消息。这将允许用户查看表达式、解决问题并再次运行程序。
而已!您已经使用 Python 的eval()
. 要运行该应用程序,请打开系统的命令行并键入以下命令:
$ python3 mathrepl.py
此命令将启动数学表达式计算器的命令行界面(CLI)。您会在屏幕上看到类似这样的内容:
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>>
到达那里后,您可以输入和计算任何数学表达式。例如,键入以下表达式:
mr>> 25 * 2
The result is: 50
mr>> sqrt(25)
The result is: 5.0
mr>> pi
The result is: 3.141592653589793
如果您输入有效的数学表达式,则应用程序会对其进行评估并将结果打印到您的屏幕上。如果您的表达式有任何问题,那么应用程序会告诉您:
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()
,这是不允许的,您会收到一条解释性错误消息。最后,您调用math
具有无效输入值的函数,应用程序会生成一条消息,标识您输入中的问题。
好了,您的数学表达式评估器已准备就绪!随意添加一些额外的功能。一些帮助您入门的想法包括扩大允许名称的字典和添加更详细的警告消息。试一试,并在评论中告诉我们进展如何。
结论
您可以使用 Python从基于字符串或基于代码的输入eval()
评估 Python表达式。当您尝试动态计算 Python 表达式并且希望避免从头开始创建自己的表达式计算器的麻烦时,此内置函数非常有用。
在本教程中,您学习了eval()
工作原理以及如何安全有效地使用它来评估任意 Python 表达式。
您现在可以:
- 使用 Python
eval()
动态评估基本 Python 表达式 - 运行更复杂的语句像函数调用,对象的创建和属性访问使用
eval()
- 最小化与使用 Python 相关的安全风险
eval()
此外,您还编写了一个应用程序,该应用程序用于使用命令行界面eval()
以交互方式计算数学表达式。您可以通过单击以下链接下载应用程序的代码:
- 点赞
- 收藏
- 关注作者
评论(0)