使用Python构建简单的表达式求值器

2025年3月3日 | 13 分钟阅读

表达式求值是许多编程语言中的一个基本概念,它允许程序解析和计算数学或逻辑表达式的结果。通过在 Python 中构建一个简单的表达式求值器,我们进入了语言解释器和编译器的领域,深入探究计算机如何处理和计算诸如 10 + 7 * 1 之类的算术语句。

在其核心,表达式求值器读取一个输入表达式,解释其含义,并输出一个结果。虽然现代语言和库有内置函数来处理这个问题,但从零开始构建一个可以让我们更深入地理解关键的计算机科学概念,例如解析、递归和运算顺序(通常称为运算符优先级)。

在本教程中,我们将探讨如何在 Python 中设计和实现一个处理简单算术运算的基本表达式求值器。我们将涵盖以下主题:

  • 表达式的词法分析(将输入分解为可管理的部分),
  • 解析和理解运算符优先级,
  • 处理加法、减法、乘法和除法等基本算术运算。

设置堆栈

首先,我们创建一个名为 self.stack = [] 的空列表。可以将这个列表看作是我们处理数学表达式时临时存放数字和运算符(如 + 或 *)的地方。

理解运算符优先级

由于我们需要处理基本的数学运算,我们构建了一个名为 _precedence 的函数。该函数为不同的运算符分配优先级(例如,乘法的优先级高于加法)。另一个函数 _apply_operator 用于实际进行数学计算——接收两个数字并应用加法或除法等运算。

求值表达式

该过程的核心是一个名为 evaluate_expression 的方法。此方法用于计算以常规(中缀)方式编写的数学问题,例如 2 + 3 * 4。它处理像 +、-、* 和 / 这样的运算符。在幕后,该方法使用 Shunting Yard 算法将中缀表达式转换为一种称为后缀的更简单的格式,这使得计算最终结果更加容易。

中缀转后缀

首先,程序建立一个基本算术运算符(如 +、-、* 和 /)的列表,用于求值表达式。输入表达式通常以中缀形式书写(即人类书写数学表达式的正常方式,运算符位于数字之间),然后被分解为独立的部分——这些部分可以是数字、运算符或括号。

从中缀到后缀(也称为逆波兰表示法)的转换是通过使用 Shunting Yard 算法实现的。该算法实质上是重新排列表达式,使运算符出现在其操作数(数字)之后,这使得计算机更容易处理。

当算法处理表达式的每个部分时,会使用一个堆栈来临时存储运算符。这确保了在将它们放入最终的后缀表达式之前,运算的顺序得到遵守。

求值后缀表达式

一旦表达式为后缀形式,程序就开始求值以找出结果。其工作原理如下:整数直接添加到堆栈中,每当遇到一个运算符时,它就被应用于堆栈中已有的数字。这种方法既考虑了运算符的优先级(重要性顺序,如乘法先于加法),也考虑了结合性(它们是从左到右还是从右到左)。

所有操作完成后,结果从堆栈中取出。

处理错误和异常

为避免处理过程中的问题,该实现会检查输入表达式是否有效。例如,如果查询中的括号不匹配(例如,一个左括号没有对应的右括号),软件将引发一个错误——具体来说是 ValueError——并解释出错的原因。

代码

输出

 
Result for '10 + 7 * 1': 17.0   

代码解释

您代码中的 MathExpressionSolver 对象旨在分析以中缀表示法编写的方程,例如 "10 + 7 * 1"。以下是其工作原理的深入描述:

  • 该类通过利用 Shunting Yard 技术将中缀方程转换为后缀格式(逆波兰表示法,RPN)来分析表达式,然后分析得到的后缀表达式。
  • operands_stack: 这个堆栈在转换期间持有运算符和括号,并用于计算。
  • __init__(): 初始化一个用于操作数的空堆栈。
  • _get_precedence(operator): 返回给定运算符的优先级。
  • 运算符 + 和 - 的优先级为 1。
  • 运算符 * 和 / 的优先级为 2。
  • 括号和未知运算符的优先级为 0。
  • _process_operator(operator): 将给定的运算符应用于堆栈中最近的两个操作数。
  • 从 operands_stack 中弹出两个操作数,应用运算符,并将结果推回堆栈。
  • 如果操作数不足,则引发异常。
  • compute(expr): 求值表达式的主要方法。
  • 遍历表达式中的标记(token)。
  • 数字直接添加到 postfix_expr。
  • 在弹出具有更高或相等优先级的运算符后,将运算符推入 operands_stack。
  • 括号管理分组;括号内的运算符优先处理。
  • 处理后缀表达式中的标记,按其出现的顺序应用运算符。
  • 计算的结果是 operands_stack 中剩下的唯一值。

示例用法

对于表达式 "10 + 7 * 1"

转换为后缀表示法

  • 解析 "10",添加到 postfix_expr。
  • 解析 "+",推入 operands_stack。
  • 解析 "7",添加到 postfix_expr。
  • 解析 "*",处理优先级
  • 由于 "*" 的优先级高于 "+",将 "*" 推入 operands_stack。
  • 解析 "1",添加到 postfix_expr。
  • operands_stack 中剩余的运算符被弹出并添加到 postfix_expr。

得到的 postfix_expr 是 [10, 7, 1, '*', '+']。

求值后缀表达式

  • 10 和 7 被推入堆栈。
  • 应用 "*",将 7 和 1 相乘,结果为 7。
  • 将 7 推入堆栈。
  • 应用 "+",将 10 和 7 相加,结果为 17。

Python 中简单表达式求值器的优点

在 Python 中,一个简单的表达式求值器对于某些任务非常有用。以下是一些优点:

1. 易于实现

描述:一个基本的表达式求值器可以用最少的编码工作来创建,通常利用 Python 的内置函数或简单的解析技术。

基本工具:您可以使用 Python 内置的 eval() 函数直接求值表达式。例如,eval("3 + 4") 返回 7。

自定义解析器:为获得更多控制,您可以实现一个自定义解析器。这可能涉及对表达式进行词法分析,并应用像 Shunting Yard 算法这样的基本算法来处理运算符优先级。

优点

  • 简单性:实现不需要高级编程概念,对初学者很友好。
  • 速度:其直接的特性使得开发和原型制作非常迅速。

2. 执行速度快

描述:简单的求值器能高效地处理表达式,使其适用于不涉及复杂计算或大数据集的任务。

详细信息

  • 最小化处理:对于直接的数学运算或基本逻辑,求值器执行的计算量很小。
  • 示例:由于过程的基本简单性,求值像 "5 * (2 + 3)" 这样的简单数学公式会很快完成。

优点

  • 响应性:非常适合需要快速反馈的实时应用,如交互式计算器或轻量级脚本。
  • 效率:求值器的简单性最小化了处理时间,有利于性能。

3. 依赖项最少

描述:基本的求值器通常不依赖外部库或包,降低了设置和维护的复杂性。

详细信息

  • 自包含:求值器可以仅使用 Python 核心功能实现,避免了需要额外的依赖项。
  • 示例:一个使用 eval() 或基本字符串操作的简单求值器不需要额外的库。

优点

  • 易用性:无需安装或管理外部包,简化了开发环境。
  • 可移植性:求值器可以轻松地在安装额外软件不方便的环境中共享或部署。

4. 减少资源使用

描述:简单的求值器设计为使用最少的系统资源,使其适用于受限环境。

详细信息

  • 低开销:它们需要最少的内存和处理能力,因为它们不涉及复杂的算法或大规模数据处理。
  • 示例:与更复杂的计算任务相比,用于算术表达式的基本求值器通常使用可忽略不计的内存。

优点

  • 效率:在低资源环境或在能力有限的设备上运行时尤其有益。
  • 可扩展性:适用于需要高效率的小规模应用。

5. 适用于基本用例的灵活性

描述:尽管简单,但基本的求值器可以适应一系列直接的任务。

详细信息

  • 自定义函数:您可以根据需要扩展求值器以处理自定义运算符或函数。
  • 示例:增加对基本三角函数或预定义常量的支持可以增强功能,而不会显著增加复杂性。

优点

  • 适应性:可以根据特定要求或用例进行定制,例如简单的数据转换或基于规则的系统。
  • 定制化:允许进行直接调整以满足不同应用的需求。

6. 教育价值

描述:实现一个简单的表达式求值器可以为基本编程概念提供宝贵的见解。

详细信息

  • 学习机会:它涉及理解表达式解析、词法分析和求值,这些是编程和计算机科学的基础主题。
  • 示例:实现自己的求值器有助于从更底层的角度理解表达式求值的工作原理,而不是使用高级函数。

优点

  • 技能发展:增强对核心编程原理和算法的理解。
  • 实践经验:提供解析和求值技术的实践经验。

7. 可定制性

描述:一个简单的求值器可以轻松定制以适应特定的需求或偏好。

详细信息

  • 可调整的逻辑:您可以修改求值器以处理不同类型的表达式或将其与应用程序的其他组件集成。
  • 示例:通过基本实现,增加对自定义运算符或用户定义函数的支持是直接的。

优点

  • 灵活性:允许根据不断变化的需求进行修改和扩展。
  • 控制力:提供对表达式如何被求值和处理的更大控制。

8. 调试和测试

描述:与更复杂的系统相比,基本求值器的简单性使其更易于调试和测试。

详细信息

  • 范围集中:组件更少,复杂性更低,调试涉及的潜在问题也更少。
  • 示例:在一个简单的求值器中测试基本功能和检测故障比在复杂的系统中更容易。

优点

  • 易于故障排除:简化了检测和纠正问题的过程。
  • 高效测试:由于复杂性降低,测试过程得以简化。

9. 低开销

描述:一个简单的求值器引入的开销最小,使其易于集成到现有系统中。

详细信息

  • 集成:可以添加到应用程序或脚本中,而无需对现有代码库进行重大更改。
  • 示例:一个基本的表达式求值器可以作为大型应用程序中的一个组件使用,对整体性能的影响最小。

优点

  • 无缝集成:易于并入现有项目或工作流程。
  • 最小干扰:不需要对现有系统进行广泛修改。

Python 中的简单表达式求值器提供了多种优势,包括易于实现、执行速度快、依赖项最少和资源使用减少。其教育价值、可定制性和低开销使其成为基本任务和学习目的的实用工具。虽然它可能不适用于复杂场景,但其优点使其在各种环境中成为一个有价值的工具。

Python 中简单表达式求值器的缺点

Python 中的简单求值器——本质上是用于评估或评分代码、性能或其他指标的基本系统——可能存在几个缺点:

  1. 有限的灵活性
    一个简单的求值器通常被设计为处理一组狭窄的评估标准或特定场景。当需要修改评估以适应新标准或更复杂场景时,这种僵化可能会带来麻烦。例如,如果您正在评估代码质量,而您简单的评估只查找基本语法问题,那么它将忽略代码效率或是否遵循推荐实践等额外变量。
  2. 缺乏鲁棒性
    简单的求值器可能没有全面的错误处理机制。在处理多样化且可能错误的输入时,它们可能无法优雅地管理异常或意外情况。例如,如果一个求值器旨在对用户输入进行评分,但遇到了无效或恶意的输入,它可能会崩溃或产生不可靠的结果。
  3. 可扩展性问题
    可扩展性是简单求值器的一个重要问题。随着评估范围的扩大——无论是在数据量还是复杂性方面——求值器可能会变得缓慢或效率低下。例如,一个执行基本检查的简单求值器可能可以高效地处理少量项目,但在处理大型数据集时会遇到困难,导致处理时间过长或资源消耗过多。
  4. 结果不准确
    一个简单的求值器可能不使用复杂的算法或分析方法,可能导致错误的评估。例如,一个衡量机器学习模型性能的简单求值器可能只评估像准确率这样的基本指标,而忽略了像精确率、召回率或 F1 分数这样对于全面评估至关重要的更复杂特征。
  5. 维护挑战
    随着需求的变化或新功能的需要,一个简单的求值器可能变得难以维护。添加新的评估标准或调整现有标准可能需要大量的代码更改,可能引入错误或降低求值器的稳定性。如果求值器的初始设计没有预见到未来的修改,这尤其具有挑战性。
  6. 功能缺失
    高级求值器通常具备详细报告、结果可视化以及与其他工具集成等功能。一个基本的求值器可能缺乏这些特性,使得客户更难从评估结果中获取有意义的信息。消费者可能会因缺乏视觉辅助而难以识别信息中的模式或趋势。
  7. 安全担忧
    如果一个简单的求值器执行或处理用户提供的代码或数据,则存在潜在的安全风险。例如,如果求值器没有正确地沙箱化或验证输入,它可能容易受到代码注入攻击或其他安全漏洞的攻击。这可能会损害求值器的完整性或其所属系统的安全性。
  8. 用户体验差
    如果一个简单的求值器缺乏用户友好的界面或清晰的反馈机制,用户体验可能会受到影响。用户可能会发现难以理解他们的输入是如何处理的,以及他们如何理解结果。例如,如果评估者仅仅提供一个数字分数而没有任何背景或解释,用户可能会难以理解这个数字的含义或如何提高他们的表现。
    简单求值器对于简单任务是有效的,但在更复杂或动态的环境中它们往往显得不足。解决这些缺点通常需要采用更复杂的计算,增加错误处理,提高可扩展性,并引入能够带来更好用户界面和更精确评估的功能。

Python 中简单表达式求值器的应用

Python 中的简单表达式求值器可以应用于各种实际和教育场景。以下是一些常见的应用:

1. 计算器应用

描述:一个基本的表达式求值器可以用来创建用于执行算术运算的简单计算器。

示例

  • 交互式计算器:一个处理用户输入如 "12 + 7 * (3 - 1)" 以提供结果的求值器。
  • 单位转换:用于使用表达式转换单位(例如,英寸到厘米)的简单计算器。

用例

  • 教育工具:用于教授算术和代数的学习辅助工具。
  • 个人实用工具:用于日常任务的快速计算。

2. 配置解析

描述:求值器可用于解释和处理表示为数学或逻辑表达式的配置设置。

示例

  • 动态配置:配置值基于其他参数动态计算的系统(例如,"base_rate * 1.2")。

用例

  • 软件配置:允许用户使用表达式指定设置的工具,然后对这些表达式进行求值以确定配置参数。

3. 数学问题求解器

描述:简单的求值器可以处理基本的数学问题,使其在教育工具或问题解决应用中非常有用。

示例

  • 作业助手:帮助学生解决代数方程或求值表达式的工具。
  • 数学谜题:解决和检查数学谜题的答案。

用例

  • 教育平台:提供练习题和解答的网站或应用。

4. 脚本和自动化

描述:求值器可以集成到脚本中,以自动化涉及计算或条件逻辑的任务。

示例

  • 自动化报告:根据配置文件中定义的表达式生成带有计算值的报告。
  • 任务调度:根据动态条件调度任务(例如,"current_time + timedelta(days=7)")。

用例

  • 系统管理:根据计算调整参数或设置的脚本。

5. 应用中的公式

描述:用于用户可以为各种目的定义公式的应用中,例如财务计算或数据分析。

示例

  • 金融计算器:用于根据用户定义的公式计算利息、贷款或投资回报的应用。
  • 电子表格替代品:用于执行类似于电子表格软件中计算的简单工具。

用例

  • 金融:用于预算、投资分析或财务规划的工具。

6. 基本验证和过滤

求值器可用于评估输入或根据以简单术语呈现的既定标准过滤数据。

示例

  • 输入验证涉及确定用户输入是否在指定范围内或满足某些约束条件(例如,"value > 10 and value < 100")。
  • 数据过滤:将基于表达式的过滤器应用于数据集(例如,选择 "age > 21" 的条目)。

用例

  • 用户界面:需要简单验证规则或数据过滤的表单和应用。

7. 游戏开发

描述:简单的求值器可用于游戏开发中处理基本的游戏逻辑或配置。

示例

  • 计分系统:根据表达式评估分数或表现(例如,"base_score + level * 10")。
  • 规则引擎:实现定义为表达式的基本游戏规则或条件。

用例

  • 游戏机制:用于定义和调整游戏机制和计分的工具。

8. 教育练习

描述:在教育情境中实现和使用简单的表达式求值器,以演示编程和计算的概念。

示例

  • 编程练习:用于教授解析、求值和算术等编程概念的练习。
  • 交互式学习:允许学生试验表达式并查看实时结果的工具。

用例

  • 教学:用于学习编程和计算逻辑的资源和工具。

Python 中的简单表达式求值器应用广泛,从计算器工具和配置解析到教育辅助和基本自动化。其灵活性和易于实现使其成为处理直接计算、动态配置和教育练习的宝贵工具。

结论

总之,在 Python 中创建一个简单的表达式求值器为解析和评估数学表达式提供了一个实用的入门。通过利用 Python 的固有能力,例如其处理字符串操作和算术运算的能力,我们可以开发一个功能性的求值器来处理带有基本运算符的表达式。这个项目不仅展示了基本的编程技能,还突出了仔细处理用户输入和错误管理的重要性。通过开发过程,人们在设计算法和理解表达式解析的复杂性方面获得了宝贵的经验,为更高级的计算任务和编程挑战奠定了基础。