Python快速入门专业版(三十四):函数实战1:计算器程序(支持加减乘除与括号优先级)
目录
- 引
- 一、需求分析:计算器的核心功能与技术要点
- 1. 核心功能
- 2. 技术难点
- 二、模块拆分:将计算器功能拆解为小函数
- 三、代码实现:分模块编写核心函数
- 1. 基础运算函数:实现加减乘除
- 2. 表达式解析函数:拆分数字与运算符
- 3. 无括号表达式计算:处理乘除与加减优先级
- 4. 带括号表达式计算:递归消除括号
- 5. 主函数:整合功能与异常处理
- 四、代码测试与功能验证
- 1. 基础运算测试
- 2. 括号优先级测试
- 3. 负数与复杂表达式测试
- 4. 异常场景测试
- 五、代码优化与扩展思路
- 1. 代码优化
- 2. 功能扩展
- 六、实战总结:函数模块化编程的核心思想
引
在Python函数学习中,单一功能的函数练习往往难以体现实战价值。而计算器程序作为经典的综合案例,能将函数定义、表达式解析、优先级处理、异常捕获等知识点串联起来,完美展现“模块化编程”的思想——通过拆分复杂问题(如表达式计算)为多个小函数(基础运算、括号处理、优先级排序),最终组合实现完整功能。
本文将从零开始构建一个支持“加减乘除”和“括号优先级”的简易计算器,详细讲解从需求分析到代码实现的全过程,包括基础运算函数定义、表达式解析逻辑、运算符优先级处理(先乘除后加减,括号优先),以及异常处理(除数为0、格式错误等),帮助你掌握函数在实战中的综合应用。
一、需求分析:计算器的核心功能与技术要点
在编写代码前,需明确计算器的功能边界和技术难点,确保开发目标清晰。
1. 核心功能
- 支持四种基础运算:加法(+)、减法(-)、乘法(*)、除法(/)。
- 遵循数学运算优先级:括号内的表达式优先计算,其次是乘除运算,最后是加减运算。
- 支持多位数、小数(如“12.3+4.5*2”)和负数(如“-5+3”“(2-5)*4”)。
- 输入输出友好:接收用户输入的表达式字符串(如“(1+2)*3-4/2”),输出计算结果,并处理异常情况(如除数为0、表达式格式错误)。
2. 技术难点
- 表达式解析:将字符串形式的表达式(如“1+2*3”)拆分为“数字”和“运算符”两部分(如数字列表
[1,2,3]
,运算符列表['+', '*']
)。 - 括号处理:识别表达式中的最内层括号,递归计算括号内结果,并用结果替换括号部分,逐步消除括号。
- 优先级排序:在无括号的表达式中,先执行乘除运算,再执行加减运算,需动态调整数字和运算符列表。
- 异常处理:覆盖常见错误场景(除数为0、括号不匹配、非数字输入、运算符位置错误等)。
二、模块拆分:将计算器功能拆解为小函数
遵循“单一职责原则”,将计算器拆分为以下核心函数,每个函数专注于解决一个具体问题:
函数名称 | 功能描述 | 输入 | 输出 |
---|---|---|---|
add(a, b) | 实现两个数的加法 | 两个数字(int/float) | 加法结果(int/float) |
subtract(a, b) | 实现两个数的减法 | 两个数字(int/float) | 减法结果(int/float) |
multiply(a, b) | 实现两个数的乘法 | 两个数字(int/float) | 乘法结果(int/float) |
divide(a, b) | 实现两个数的除法(处理除数为0) | 两个数字(int/float) | 除法结果(int/float) |
parse_expression(expr) | 解析表达式字符串,拆分数字和运算符 | 表达式字符串(如“1+2*3”) | 元组(numbers, operators) ,分别为数字列表和运算符列表 |
calculate_no_brackets(numbers, operators) | 计算无括号的表达式(处理乘除、加减优先级) | 数字列表、运算符列表 | 计算结果(int/float) |
calculate_with_brackets(expr) | 处理带括号的表达式(递归计算括号内结果) | 表达式字符串 | 最终计算结果(int/float) |
calculator() | 主函数:接收用户输入,调用其他函数,处理异常 | 无 | 打印计算结果或错误提示 |
三、代码实现:分模块编写核心函数
1. 基础运算函数:实现加减乘除
首先编写四个基础运算函数,其中除法函数需处理“除数为0”的异常,为后续计算提供稳定的基础功能。
def add(a: float, b: float) -> float:"""实现两个数的加法运算"""return a + bdef subtract(a: float, b: float) -> float:"""实现两个数的减法运算(a - b)"""return a - bdef multiply(a: float, b: float) -> float:"""实现两个数的乘法运算"""return a * bdef divide(a: float, b: float) -> float:"""实现两个数的除法运算(a / b),处理除数为0的异常"""if b == 0:# 抛出自定义异常,便于上层函数捕获raise ZeroDivisionError("除数不能为0")return a / b
解析:
- 每个函数接收两个
float
类型参数(兼容整数和小数),返回运算结果。 divide
函数中,若除数b
为0,主动抛出ZeroDivisionError
异常,由上层函数统一处理,避免程序崩溃。
2. 表达式解析函数:拆分数字与运算符
将用户输入的表达式字符串(如“1+23”)拆分为“数字列表”和“运算符列表”,是后续计算的前提。这里使用正则表达式匹配数字(包括整数、小数、负数)和运算符(+、-、、/、(、)),高效且准确。
import redef parse_expression(expr: str) -> tuple[list[float], list[str]]:"""解析表达式字符串,提取数字和运算符(不含括号)参数:expr - 去除空格后的表达式字符串(如“1+2*3”)返回:(numbers, operators) - 数字列表和运算符列表异常:若表达式格式错误(如连续运算符、缺少数字),抛出ValueError"""# 正则表达式匹配规则:# -?\d+\.?\d*: 匹配负数(-开头)、整数(如123)、小数(如12.34)# [+\-*/]: 匹配加减乘除运算符(注意-需转义,避免与范围符号混淆)pattern = r'-?\d+\.?\d*|[+\-*/]'# 提取所有匹配项tokens = re.findall(pattern, expr)# 分离数字和运算符numbers = []operators = []for token in tokens:if token in '+-*/':operators.append(token)else:try:# 将字符串转换为数字(支持整数和小数)numbers.append(float(token))except ValueError:raise ValueError(f"无效的数字:{token}")# 验证表达式合法性(数字数量 = 运算符数量 + 1)if len(numbers) != len(operators) + 1:raise ValueError("表达式格式错误(可能存在连续运算符或缺少数字)")return numbers, operators
解析:
- 正则表达式:
r'-?\d+\.?\d*|[+\-*/]'
是核心,分为两部分:(?\d+\.?\d*)
:匹配数字,-?
表示可选负号,\d+
表示1个以上数字,\.?\d*
表示可选小数点和后续数字(兼容整数和小数)。[+\-*/]
:匹配四个运算符(注意-
需转义为\-
,避免被当作正则中的“范围符号”)。
- 合法性校验:正常表达式中,数字数量必然比运算符多1(如“a+b*c”有3个数字、2个运算符),若不满足则抛出格式错误。
- 异常传递:若提取到无法转换为数字的token(如字母),抛出
ValueError
,由上层函数处理。
3. 无括号表达式计算:处理乘除与加减优先级
对于无括号的表达式(如“1+2*3-4/2”),需遵循“先乘除后加减”的优先级。实现思路是:
- 先遍历运算符列表,处理所有“*”和“/”,计算对应结果后更新数字和运算符列表。
- 再遍历剩余的运算符列表(仅含“+”和“-”),计算最终结果。
def calculate_no_brackets(numbers: list[float], operators: list[str]) -> float:"""计算无括号的表达式,遵循“先乘除后加减”的优先级参数:numbers - 数字列表(如[1,2,3,4])operators - 运算符列表(如['+', '*', '-', '/'])返回:表达式的计算结果异常:若运算符不合法或除数为0,抛出对应异常"""# 复制列表,避免修改原列表(函数应保持“纯函数”特性,不影响外部数据)nums = numbers.copy()ops = operators.copy()# 第一步:处理乘除运算(优先级高)i = 0while i < len(ops):op = ops[i]if op in ('*', '/'):# 获取当前运算符左右两侧的数字a = nums[i]b = nums[i + 1]# 根据运算符调用对应函数if op == '*':result = multiply(a, b)else: # op == '/'result = divide(a, b)# 更新数字列表:用计算结果替换a和b(如[1,2,3] → [1,6])nums[i] = resultdel nums[i + 1]# 更新运算符列表:移除已处理的运算符del ops[i]# 无需i+1,因为列表长度已缩短else:# 若为加减运算符,暂不处理,继续遍历i += 1# 第二步:处理加减运算(优先级低,此时运算符列表仅含'+'和'-')result = nums[0] # 初始值为第一个数字for i in range(len(ops)):op = ops[i]b = nums[i + 1]if op == '+':result = add(result, b)else: # op == '-'result = subtract(result, b)return result
解析:
- 列表复制:通过
nums = numbers.copy()
和ops = operators.copy()
避免修改外部传入的列表,确保函数是“纯函数”(输入不变则输出不变,无副作用)。 - 乘除处理:遍历运算符列表,遇到“*”或“/”时,计算对应两个数字的结果,用结果替换这两个数字(如
[1,2,3]
→[1,6]
),并删除已处理的运算符,实现列表“收缩”。 - 加减处理:乘除处理后,运算符列表仅含“+”和“-”,从左到右依次计算即可。
4. 带括号表达式计算:递归消除括号
括号的优先级最高,需先计算最内层括号内的表达式。实现思路是:
- 用正则表达式找到最内层括号(即不包含其他括号的
(...)
)。 - 提取括号内的表达式,调用
parse_expression
和calculate_no_brackets
计算结果。 - 用计算结果替换原表达式中的括号部分(如“(1+2)3”→“33”)。
- 重复步骤1-3,直到表达式中无括号,最后计算无括号表达式的结果。
def calculate_with_brackets(expr: str) -> float:"""计算带括号的表达式,通过递归消除括号后计算参数:expr - 去除空格后的表达式字符串(如“(1+2)*3-4/2”)返回:表达式的最终计算结果异常:若括号不匹配或表达式格式错误,抛出ValueError"""# 正则表达式匹配最内层括号:\([^()]*\)# \(: 匹配左括号;[^()]*: 匹配不含括号的任意字符(最内层括号特征);\): 匹配右括号bracket_pattern = r'\([^()]*\)'# 循环消除括号,直到表达式中无括号while '(' in expr or ')' in expr:# 查找最内层括号match = re.search(bracket_pattern, expr)if not match:# 若存在括号但未找到匹配(如括号不闭合),抛出异常raise ValueError("括号不匹配(可能存在未闭合或嵌套错误)")# 提取括号内的表达式(如“(1+2)”→“1+2”)bracket_content = match.group()[1:-1] # [1:-1] 去除左右括号# 解析括号内的表达式并计算结果try:nums, ops = parse_expression(bracket_content)bracket_result = calculate_no_brackets(nums, ops)except ValueError as e:raise ValueError(f"括号内表达式错误:{e}")# 用计算结果替换原括号部分(如“(1+2)”→“3”)expr = expr.replace(match.group(), str(bracket_result))# 表达式已无括号,直接计算结果nums, ops = parse_expression(expr)return calculate_no_brackets(nums, ops)
解析:
- 最内层括号匹配:正则表达式
r'\([^()]*\)'
是关键,[^()]
表示“除括号外的任意字符”,确保只匹配不包含其他括号的最内层括号(如“(a+(bc))”中先匹配“(bc)”)。 - 递归思想:通过循环而非显式递归,逐步消除括号,每次处理最内层,最终将带括号表达式转化为无括号表达式,降低计算复杂度。
- 括号校验:若表达式中存在括号但未找到匹配(如“(1+2”缺少右括号),抛出“括号不匹配”异常。
5. 主函数:整合功能与异常处理
主函数calculator()
负责串联所有模块:接收用户输入、预处理(去除空格)、调用计算函数、捕获异常并输出结果,是计算器与用户交互的入口。
def calculator():"""计算器主函数:接收用户输入,处理表达式计算,输出结果或错误提示"""print("=" * 50)print(" 简易计算器(支持加减乘除与括号优先级)")print("说明:输入表达式(如“(1+2)*3-4/2”),输入“q”退出")print("=" * 50)while True:# 接收用户输入user_input = input("\n请输入表达式:").strip()# 退出逻辑if user_input.lower() == 'q':print("感谢使用,再见!")break# 预处理:去除表达式中的所有空格(如“1 + 2 * 3”→“1+2*3”)expr = user_input.replace(" ", "")# 空输入处理if not expr:print("错误:请输入有效的表达式")continuetry:# 核心计算逻辑result = calculate_with_brackets(expr)# 输出结果(若为整数,去除小数点后多余的0,如6.0→6)if result.is_integer():print(f"计算结果:{int(result)}")else:print(f"计算结果:{result:.2f}") # 小数保留2位except ZeroDivisionError as e:print(f"计算错误:{e}")except ValueError as e:print(f"格式错误:{e}")except Exception as e:# 捕获其他未预料的异常,避免程序崩溃print(f"未知错误:{str(e)}")# 运行计算器
if __name__ == "__main__":calculator()
解析:
- 用户交互:通过循环持续接收输入,输入“q”时退出,符合计算器的使用习惯。
- 输入预处理:用
replace(" ", "")
去除所有空格,支持用户输入带空格的表达式(如“1 + (2 * 3)”)。 - 异常捕获:
ZeroDivisionError
:处理除数为0的错误。ValueError
:处理表达式格式错误(如括号不匹配、连续运算符)。- 通用
Exception
:捕获其他未预料的错误(如内存不足),确保程序稳定运行。
- 结果格式化:若结果为整数(如6.0),转换为
int
类型输出(避免“6.0”的冗余显示);若为小数,保留2位小数(如2.50→2.50,1.3333→1.33)。
四、代码测试与功能验证
运行计算器程序后,通过以下测试案例验证核心功能:
1. 基础运算测试
- 输入:
1+2*3
→ 预期结果:7(先算2*3=6,再算1+6=7) - 输入:
10-4/2
→ 预期结果:8(先算4/2=2,再算10-2=8) - 输入:
3.5*2+1.5
→ 预期结果:8.5(3.5*2=7,7+1.5=8.5)
2. 括号优先级测试
- 输入:
(1+2)*3
→ 预期结果:9(先算括号内1+2=3,再算3*3=9) - 输入:
10/(2+3)
→ 预期结果:2(先算括号内2+3=5,再算10/5=2) - 输入:
(3+2)*(4-1)
→ 预期结果:15(先算3+2=5、4-1=3,再算5*3=15)
3. 负数与复杂表达式测试
- 输入:
-5+3*2
→ 预期结果:1(先算3*2=6,再算-5+6=1) - 输入:
(2-5)*4+8/2
→ 预期结果:-8(先算2-5=-3,-3*4=-12;再算8/2=4;最后-12+4=-8)
4. 异常场景测试
- 输入:
10/0
→ 预期提示:计算错误:除数不能为0
- 输入:
1++2
→ 预期提示:格式错误:表达式格式错误(可能存在连续运算符或缺少数字)
- 输入:
(1+2*3
→ 预期提示:格式错误:括号不匹配(可能存在未闭合或嵌套错误)
- 输入:
a+1
→ 预期提示:格式错误:无效的数字:a
五、代码优化与扩展思路
当前计算器已实现核心功能,可从以下方面优化和扩展:
1. 代码优化
- 支持更多运算符:如取模(%)、幂运算(**),只需在
parse_expression
的正则表达式中添加运算符,在calculate_no_brackets
中增加对应处理逻辑。 - 优化正则表达式:当前正则对负数的匹配可能存在边缘情况(如“1±2”),可优化为
r'(?<![+\-*/(])-?\d+\.?\d*|[+\-*/()]'
,避免将“1±2”中的“-”误判为负数符号。 - 缓存计算结果:对于重复输入的表达式(如用户多次输入“(1+2)*3”),可缓存计算结果,减少重复解析和计算时间。
2. 功能扩展
- 支持历史记录:用列表存储用户输入的表达式和结果,添加“查看历史”功能(输入“history”显示历史记录)。
- 支持变量赋值:允许用户定义变量(如“a=10”),后续表达式可使用变量(如“a+2*3”)。
- 图形界面(GUI):使用
tkinter
或PyQt
库开发图形界面,替代命令行输入,提升用户体验。
六、实战总结:函数模块化编程的核心思想
通过计算器程序的开发,我们可以提炼出函数实战的核心原则:
- 拆分复杂问题:将“表达式计算”这个复杂问题拆分为“基础运算”“表达式解析”“括号处理”“优先级排序”等小问题,每个问题用一个函数解决,降低思维复杂度。
- 保持函数单一职责:每个函数只做一件事(如
parse_expression
仅负责拆分数字和运算符,不参与计算),让函数易于理解、测试和维护。 - 函数间低耦合:函数通过参数和返回值传递数据,避免直接修改外部变量(如
calculate_no_brackets
复制列表而非修改原列表),提高代码的可复用性。 - 全面异常处理:预判可能的错误场景(如除数为0、格式错误),用异常捕获机制确保程序稳定,同时给用户友好的提示。
这种“模块化编程”思想不仅适用于计算器开发,更适用于所有复杂程序(如管理系统、数据分析工具)。掌握它,能让你写出更清晰、更健壮、更易扩展的代码。