欢迎光临
我们一直在努力

代码越写越乱怎么办,用函数和模块把记账本重构一遍

从“流水账”到“工程师”:用函数重构你的记账本

从“流水账”到“工程师”:用函数重构你的记账本

还记得第五课结束时,那个能永久保存数据的记账本吗?当时我们看着数据成功写入 records.txt,心里肯定挺有成就感。但如果你回头去审视那段代码,可能会发现一个尴尬的事实:随着功能增加,代码越来越长,所有的逻辑——输入判断、数据计算、文件读写——全都挤在 while True 的主循环里。

这种写法在编程界有个形象的称呼:“面条代码”。就像一碗纠缠在一起的意大利面,你想挑出一根(修改某个功能),往往会带起一大团(影响其他逻辑)。比如,如果你想把“记录格式”从简单的逗号分隔改成 JSON 格式,你可能需要在主循环里到处查找并修改字符串拼接的代码;如果未来想增加“删除记录”的功能,主循环里的 if-elif-else 分支会膨胀到让人头晕。

这就是新手进阶必须跨越的一道坎:代码复用与结构组织。今天,我们就通过引入**函数(Function)和模块(Module)**这两个核心概念,把那个臃肿的记账本彻底重构一遍。你会发现,原本几百行令人窒息的代码,瞬间变得清爽、专业,而且易于维护。

动手体验:一个完整的函数示例

在深入理论之前,我们先通过一个完整的、可直接运行的代码示例,直观感受函数如何让代码变得清晰和可复用。这个示例演示了如何将“计算商品总价”这个常见任务封装成函数。

# 示例:使用函数计算购物车总价
def calculate_total_price(item_prices, discount_rate=0.0):
"""
计算购物车中所有商品的总价,并应用折扣。

参数:
item_prices (list): 一个包含商品价格的列表,例如 [100, 200, 50]
discount_rate (float): 折扣率,范围 0.0 到 1.0。例如 0.1 代表 10% 折扣。

返回:
float: 应用折扣后的最终总价。
"""
# 1. 计算原始总价
subtotal = sum(item_prices)
print(f"商品小计:{subtotal:.2f} 元")

# 2. 计算折扣金额
discount_amount = subtotal * discount_rate
if discount_amount > 0:
print(f"折扣金额:-{discount_amount:.2f} 元")

# 3. 计算最终总价
final_price = subtotal discount_amount
return final_price

# 主程序:调用函数
if __name__ == "__main__":
# 模拟购物车中的商品价格
my_cart = [299.99, 150.0, 49.9, 80.5]

# 调用函数,不打折
total_without_discount = calculate_total_price(my_cart)
print(f"最终价格(无折扣):{total_without_discount:.2f} 元")
print("-" * 30)

# 再次调用函数,应用 15% 折扣
total_with_discount = calculate_total_price(my_cart, discount_rate=0.15)
print(f"最终价格(85折后):{total_with_discount:.2f} 元")

运行结果:

商品小计:580.39 元
最终价格(无折扣):580.39 元
——————————
商品小计:580.39 元
折扣金额:-87.06 元
最终价格(85折后):493.33 元

函数调用流程示意图:

为了更直观地理解函数调用的完整过程,下面用流程图展示从主程序调用 calculate_total_price 函数开始,到最终返回结果的完整流程:

渲染错误: Mermaid 渲染失败: Parse error on line 4:
…小计subtotal = sum(item_prices)] D
———————–^
Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'PS'

流程图解读:

  • 主程序调用:主程序通过 calculate_total_price(my_cart) 调用函数,将 my_cart 列表作为参数传入。
  • 参数传递:函数接收到 item_prices 参数(值为 [299.99, 150.0, 49.9, 80.5])和默认参数 discount_rate=0.0。
  • 函数内部执行:
    • 步骤1:计算小计 sum(item_prices) = 580.39
    • 步骤2:打印小计信息
    • 步骤3:判断是否有折扣(第一次调用 discount_rate=0.0,跳过折扣计算)
    • 步骤4:计算最终价格 final_price = subtotal – discount_amount
  • 返回值:通过 return final_price 将计算结果返回给主程序。
  • 主程序继续:主程序接收到返回值后,打印结果并继续执行后续代码。
  • 第二次调用:同样的流程再次执行,但这次传入 discount_rate=0.15,所以会计算并显示折扣信息。
  • 这个流程图清晰地展示了函数如何作为一个"黑盒子"工作:主程序提供输入(参数),函数内部处理逻辑,最后返回输出(返回值)。这种"输入-处理-输出"的模式正是函数的核心价值所在。

    代码解读:

  • 函数定义 (def calculate_total_price):我们定义了一个名为 calculate_total_price 的函数,它有两个参数:item_prices(商品价格列表)和 discount_rate(折扣率,默认值为 0.0,表示不打折)。
  • 文档字符串 (""" … """):函数开头的三引号注释清晰地说明了函数的作用、参数含义和返回值。这是好习惯,能让别人(或未来的你)快速理解函数功能。
  • 函数体:内部逻辑分为三步:计算小计、计算折扣、计算最终价格。print 语句用于向用户展示中间过程。
  • return 语句:函数通过 return final_price 将计算结果“吐”出来,交给调用者。
  • 函数调用:在主程序中,我们两次调用了同一个函数。第一次只传了价格列表,使用了默认的零折扣。第二次传入了列表和 0.15 的折扣率。这就是函数的威力:同一段逻辑,通过不同的输入,得到不同的输出,完全无需重复写计算代码。
  • 通过这个简单的例子,你应该能体会到函数如何将一段固定的逻辑“打包”成一个可复用的工具。接下来,我们将把这种思想应用到整个记账本项目中。

    还记得第五课结束时,那个能永久保存数据的记账本吗?当时我们看着数据成功写入 records.txt,心里肯定挺有成就感。但如果你回头去审视那段代码,可能会发现一个尴尬的事实:随着功能增加,代码越来越长,所有的逻辑——输入判断、数据计算、文件读写——全都挤在 while True 的主循环里。

    这种写法在编程界有个形象的称呼:“面条代码”。就像一碗纠缠在一起的意大利面,你想挑出一根(修改某个功能),往往会带起一大团(影响其他逻辑)。比如,如果你想把“记录格式”从简单的逗号分隔改成 JSON 格式,你可能需要在主循环里到处查找并修改字符串拼接的代码;如果未来想增加“删除记录”的功能,主循环里的 if-elif-else 分支会膨胀到让人头晕。

    这就是新手进阶必须跨越的一道坎:代码复用与结构组织。今天,我们就通过引入**函数(Function)和模块(Module)**这两个核心概念,把那个臃肿的记账本彻底重构一遍。你会发现,原本几百行令人窒息的代码,瞬间变得清爽、专业,而且易于维护。

    为什么我们需要函数:拒绝重复,拥抱封装

    在重构之前,我们先聊聊为什么要用函数。在你的旧版记账本中,是否出现过这样的场景:

  • 重复的逻辑:每次用户输入金额时,你都要写一遍 try…except 来防止用户输入字母导致程序崩溃。
  • 混乱的主线:主程序里既有“询问用户要做什么”的交互逻辑,又有“计算总金额”的业务逻辑,还有“打开文件写入”的底层操作。
  • 难以测试:如果你想单独测试“计算总额”的功能是否正确,却不得不运行整个程序,一步步输入数据直到触发该功能。
  • 函数的出现,就是为了解决这些问题。它允许我们将一段完成特定任务的代码“打包”,起个名字,然后在任何需要的地方直接调用。这不仅消除了重复代码(DRY 原则:Don’t Repeat Yourself),更重要的是实现了关注点分离:主程序只负责流程控制,具体的脏活累活交给函数去处理。

    定义函数的核心语法

    在 Python 中,使用 def 关键字来定义函数。让我们看一个最基础的结构:

    def greet_user(name):
    """向用户打招呼"""
    message = f"Hello, {name}! Welcome to the Accounting Book."
    return message

    这里有几个关键点需要注意,它们是你从“写脚本”转向“写工程”的基础:

    • def 关键字:告诉 Python 接下来要定义一个函数。
    • 函数名:遵循蛇形命名法(snake_case),如 greet_user,见名知意。
    • 参数:括号内的 name 是参数,相当于函数的“输入口”。函数可以没有参数,也可以有多个。
    • 文档字符串(Docstring):函数体第一行的三引号内容,用于说明函数的作用。这是专业代码的标配,能让其他人(或未来的你)一眼看懂这个函数是干嘛的。
    • return 语句:函数的“输出口”。它将结果返回给调用者。如果没有 return,函数默认返回 None。

    避坑指南:新手最容易犯的两个错误

    进阶:函数调试与错误排查

    学会了函数的基本用法后,你可能会遇到一些意想不到的错误。别担心,这是每个程序员的必经之路!本节将基于记账本中的几个核心函数,列举新手常见的错误场景,并教你如何一步步排查和解决。

    错误场景一:参数传递错误

    问题描述:调用函数时参数顺序或类型不对,导致函数无法正常工作。

    典型错误示例:

    # 错误调用:参数顺序颠倒
    amount = get_float_input(3, "请输入金额:") # 第一个参数应该是 prompt,第二个才是 max_retries

    # 错误调用:参数类型不对
    records = load_records("records.txt") # 函数不需要参数,但传入了文件名

    调试步骤:

  • 检查函数定义:首先查看函数的文档字符串或定义,确认参数顺序和类型。

    def get_float_input(prompt, max_retries=3):
    """获取合法的浮点数输入"""
    # prompt: 字符串,提示信息
    # max_retries: 整数,最大重试次数

  • 使用 print 打印参数值:在函数内部添加调试语句,查看实际接收到的参数。

    def get_float_input(prompt, max_retries=3):
    print(f"[调试] 收到的参数:prompt='{prompt}', max_retries={max_retries}")
    # … 原有代码 …

  • 运行测试:调用函数并观察输出,确认参数是否正确传递。

  • 解决方案:

    • 严格按照函数定义的参数顺序调用
    • 确保参数类型匹配(字符串、整数、列表等)
    • 对于有默认值的参数,可以省略不传

    # 正确调用
    amount = get_float_input("请输入金额:") # 使用默认的 max_retries=3
    amount = get_float_input("请输入金额:", 5) # 显式指定 max_retries=5
    records = load_records() # 正确:不需要参数

    错误场景二:返回值处理不当

    问题描述:忽略了函数的返回值,或者错误地处理了 None 返回值。

    典型错误示例:

    # 错误:忽略了 get_float_input 可能返回 None
    amount = get_float_input("请输入金额:")
    total = amount * 1.1 # 如果用户连续输入错误,amount 为 None,这里会报错!

    # 错误:没有检查 save_records 的返回值
    save_records(records)
    print("保存成功!") # 如果保存失败,这里会错误地提示成功

    调试步骤:

  • 检查函数返回值:查看函数文档,了解可能的返回值。

    def get_float_input(prompt, max_retries=3):
    """返回: float or None: 成功返回浮点数,超过重试次数返回None"""

    def save_records(records):
    """返回: True 保存成功,False 保存失败"""

  • 打印返回值验证:在调用后立即打印返回值,确认其内容。

    amount = get_float_input("请输入金额:")
    print(f"[调试] get_float_input 返回值:{amount},类型:{type(amount)}")

    success = save_records(records)
    print(f"[调试] save_records 返回值:{success}")

  • 添加条件判断:根据返回值类型进行适当的处理。

  • 解决方案:

    # 正确处理 get_float_input 的返回值
    amount = get_float_input("请输入金额:")
    if amount is None:
    print("输入失败,返回主菜单")
    return # 或进行其他错误处理
    else:
    total = amount * 1.1 # 安全计算
    print(f"计算结果:{total}")

    # 正确处理 save_records 的返回值
    if save_records(records):
    print("✅ 数据保存成功!")
    else:
    print("❌ 数据保存失败,请检查文件权限或磁盘空间")

    错误场景三:文件权限和路径问题

    问题描述:save_records 或 load_records 函数因文件权限、路径不存在等问题而失败。

    典型错误现象:

    • PermissionError: 文件被其他程序占用或没有写入权限
    • FileNotFoundError: 文件路径不存在(虽然 load_records 已处理)
    • json.JSONDecodeError: 文件内容不是有效的 JSON 格式

    调试步骤:

  • 使用 try-except 捕获特定异常:精确定位错误类型。

    def save_records(records):
    try:
    with open(DATA_FILE, 'w', encoding='utf-8') as f:
    json.dump(records, f, ensure_ascii=False, indent=2)
    return True
    except PermissionError:
    print("❌ 文件被占用或无写入权限,请关闭其他程序")
    return False
    except IOError as e:
    print(f"❌ 文件操作失败:{e}")
    return False

  • 检查文件路径和权限:添加调试信息。

    import os

    def load_records():
    print(f"[调试] 尝试加载文件:{DATA_FILE}")
    print(f"[调试] 文件是否存在:{os.path.exists(DATA_FILE)}")
    print(f"[调试] 文件可读:{os.access(DATA_FILE, os.R_OK) if os.path.exists(DATA_FILE) else 'N/A'}")

    if not os.path.exists(DATA_FILE):
    print("[调试] 文件不存在,返回空列表")
    return []
    # … 原有代码 …

  • 验证 JSON 数据格式:在读取后检查数据结构。

    def load_records():
    # … 读取文件的代码 …
    try:
    data = json.load(f)
    print(f"[调试] 成功加载 {len(data)} 条记录")
    print(f"[调试] 数据类型:{type(data)}")
    if data: # 如果有数据,检查第一条记录的结构
    print(f"[调试] 第一条记录结构:{data[0].keys()}")
    return data
    except json.JSONDecodeError as e:
    print(f"[调试] JSON 解析错误:{e}")
    print("警告:数据文件损坏,已重置为空。")
    return []

  • 解决方案:

  • 增强错误处理:为不同的异常类型提供明确的错误提示。
  • 提供备用方案:如保存失败时提示用户选择其他路径。
  • 数据验证:保存前验证数据结构,加载后验证数据完整性。
  • def save_records(records):
    """增强版的保存函数,提供更多错误信息"""
    try:
    # 验证数据格式
    if not isinstance(records, list):
    print("❌ 错误:records 必须是列表")
    return False

    # 尝试保存
    with open(DATA_FILE, 'w', encoding='utf-8') as f:
    json.dump(records, f, ensure_ascii=False, indent=2)
    print(f"✅ 成功保存 {len(records)} 条记录到 {DATA_FILE}")
    return True

    except PermissionError:
    print(f"❌ 权限错误:无法写入文件 {DATA_FILE}")
    print("可能的原因:1. 文件被其他程序打开 2. 没有写入权限")
    return False

    except IOError as e:
    print(f"❌ 文件操作错误:{e}")
    return False

    except Exception as e:
    print(f"❌ 未知错误:{e}")
    return False

    调试技巧总结
  • 从简单到复杂:先用简单的测试数据验证函数基本功能,再测试边界情况。
  • 分步调试:在关键位置添加 print 语句,观察程序执行流程。
  • 隔离测试:单独调用函数进行测试,而不是运行整个程序。# 单独测试 get_float_input
    if __name__ == "__main__":
    test_amount = get_float_input("测试输入:")
    print(f"测试结果:{test_amount}")

  • 使用 Python 调试器:对于复杂问题,可以使用 pdb 模块逐行调试。import pdb

    def problematic_function():
    pdb.set_trace() # 在这里设置断点
    # … 你的代码 …

  • 记住:调试不是失败,而是学习的过程。每次解决一个 bug,你对程序的理解就更深一层。随着经验的积累,你会逐渐培养出"第六感",能更快地定位问题所在。

    在实际编写函数时,有两个错误几乎每个新手都会踩一遍,请务必记在心里。

    1. 先定义,后调用

    Python 是解释型语言,代码是从上往下执行的。这意味着,你必须在使用函数之前先定义它。如果你把调用写在定义前面,程序会直接报错 NameError。

    错误示范:

    # 还没定义就急着用
    result = calculate_total(100, 50)

    def calculate_total(income, expense):
    return income expense

    正确做法:
    始终将函数定义放在文件顶部,或者至少放在调用它们的代码之前。养成良好的习惯:先写好所有工具(函数),再开始主流程。

    2. 参数匹配与返回值陷阱

    很多新手在定义函数时加了参数,但在调用时却忘了传参,或者搞错了顺序。另外,容易忽略 return 的存在,以为函数内部打印了结果就算完事了。

    记住:函数内部的 print 只是给用户看的,return 才是给程序用的。 如果你需要拿函数的结果去做进一步计算(比如把计算出的余额存入文件),必须使用 return。

    def get_valid_number(prompt):
    while True:
    try:
    return float(input(prompt)) # 直接返回有效数字
    except ValueError:
    print("输入无效,请输入数字。")

    # 调用时接收返回值
    amount = get_valid_number("请输入金额:")
    # 如果没有 return,amount 将是 None,后续计算会报错

    站在巨人的肩膀上:巧用模块

    除了自己封装函数,Python 强大的生态还提供了丰富的模块(Module)。模块本质上就是一个 .py 文件,里面包含了很多别人写好的函数和类。我们不需要重新发明轮子,只需要通过 import 语句“借用”过来即可。

    对于记账本项目,我们至少会用到两个内置模块:

    • os:用于处理文件和目录路径(比如检查记账文件是否存在)。
    • json:相比之前的纯文本读写,JSON 格式更适合存储结构化数据(如列表字典),且自带容错性。

    导入模块主要有两种常用方式,各有适用场景:

    方式一:import 模块名

    这是最推荐的规范写法。

    import os
    import json

    # 使用时需要加上模块名前缀
    if os.path.exists('records.json'):
    data = json.load(open('records.json'))

    优点:命名空间清晰,一眼就能看出 load 是来自 json 模块的,避免了和自己定义的函数重名。

    方式二:from 模块名 import 功能名

    当你频繁使用某个模块中的特定函数,且希望代码更简洁时,可以使用这种方式。

    from os import path
    from json import load, dump

    # 使用时直接调用,无需前缀
    if path.exists('records.json'):
    data = load(open('records.json'))

    注意:不要滥用 from module import *(导入所有内容),这会把模块里几百个名字全倒进你的命名空间,极易引发冲突,也让代码阅读者困惑某个函数到底来自哪里。

    实战重构:让记账本焕然一新

    函数设计原则:写出高质量代码的三个关键

    在开始重构之前,我们先了解三个编写高质量函数的核心原则。这些原则不仅能帮你设计出更优雅的函数,还能让代码更容易维护和扩展。

    1. 单一职责原则:一个函数只做一件事

    原则说明:每个函数应该只负责完成一个明确的任务。如果一个函数做了太多事情,它就变得难以理解、测试和复用。

    记账本示例:get_float_input() 函数只负责获取有效的浮点数输入,它不关心这个数字是用来记录收入还是支出,也不负责保存数据。这种专注让它可以被 add_record()、delete_record() 等多个地方复用。

    2. 参数明确原则:清晰的输入输出接口

    原则说明:函数的参数应该尽可能少且明确,返回值应该清晰可预测。避免使用全局变量在函数间传递数据。

    记账本示例:save_records(records) 函数只有一个参数——要保存的记录列表,返回一个布尔值表示保存是否成功。调用者一看就知道需要传什么数据,也能明确知道操作结果。

    3. 文档清晰原则:自解释的代码是最好的注释

    原则说明:通过良好的函数名、参数名和文档字符串,让函数的功能一目了然。好的函数应该像一本书的目录,看一眼就知道它能做什么。

    记账本示例:calculate_balance(records) 函数名直接说明了它的功能,参数 records 表明需要记账记录,文档字符串进一步解释了它计算的是收入减去支出的结余。

    掌握了这些原则,我们就能开始动手重构了。你会发现,遵循这些原则的函数不仅更好用,也更容易组合成复杂的程序。

    理论讲完了,现在到了最激动人心的时刻。我们将把第五课那个“流水账”式的记账本,重构为模块化、函数化的专业版本。

    重构前的痛点回顾

    旧代码大概长这样(简化版):

    # 旧版:所有逻辑堆在主循环
    records = []
    while True:
    choice = input("1.记收入 2.记支出 3.查账 4.退出:")
    if choice == '1':
    # 这里有一大段输入验证、类型转换、追加列表的代码
    try:
    amount = float(input("金额:"))
    records.append({'type': 'income', 'amount': amount})
    # 还要在这里写文件打开、写入、关闭的逻辑…
    f = open('records.txt', 'a')
    f.write(f"income,{amount}\\n")
    f.close()
    except:
    print("错了!")
    elif choice == '2':
    # 这里的代码和上面 80% 相似,只是 type 变了
    # … 重复的代码 …
    elif choice == '3':
    # 又要重新打开文件读取、解析、计算总和
    # … 又是几十行 …

    问题显而易见:文件操作逻辑重复了多次,输入验证逻辑分散在各处,主循环臃肿不堪。

    重构后的优雅结构

    现在,我们将功能拆解为独立的函数。新建一个文件 accounting_book.py,我们按照“数据层 -> 业务层 -> 交互层”的顺序来组织代码。

    第一步:封装数据存取(Data Layer)

    首先解决文件读写问题。我们将文件操作隔离出来,主程序再也不需要关心文件是怎么打开的。

    import json
    import os

    DATA_FILE = 'records.json'

    def load_records():
    """加载记录,如果文件不存在则返回空列表"""
    if not os.path.exists(DATA_FILE):
    return []
    try:
    with open(DATA_FILE, 'r', encoding='utf-8') as f:
    return json.load(f)
    except (json.JSONDecodeError, IOError):
    print("警告:数据文件损坏,已重置为空。")
    return []

    def save_records(records):
    """保存记录到文件"""
    try:
    with open(DATA_FILE, 'w', encoding='utf-8') as f:
    json.dump(records, f, ensure_ascii=False, indent=2)
    return True
    except IOError as e:
    print(f"保存失败:{e}")
    return False

    看,用了 with open 上下文管理器,再也不用担心忘记 close() 了。而且无论主程序哪里需要存数据,只需调用 save_records(),哪怕将来我们要把存储方式改成数据库,也只需要修改这两个函数,主程序完全不用动。

    第二步:封装业务逻辑(Business Layer)

    接下来处理核心的记账逻辑和输入验证。

    def get_float_input(prompt, max_retries=3):
    """
    获取合法的浮点数输入,带自动重试机制和最大重试次数限制

    参数:
    prompt (str): 提示信息
    max_retries (int): 最大重试次数,默认3次

    返回:
    float or None: 成功返回浮点数,超过重试次数返回None
    """
    for attempt in range(1, max_retries + 1):
    try:
    value = input(prompt)
    return float(value)
    except ValueError:
    if attempt < max_retries:
    print(f"❌ 输入无效,请输入数字。您还有 {max_retries attempt} 次机会。")
    else:
    print(f"❌ 已连续 {max_retries} 次输入无效,请返回主菜单重新操作。")
    return None
    return None # 安全返回,实际不会执行到这里

    def add_record(records, record_type):
    """添加一条新记录"""
    amount = get_float_input(f"请输入{record_type}金额:")
    note = input("备注(可选):")

    new_record = {
    'type': record_type,
    'amount': amount,
    'note': note
    }
    records.append(new_record)
    print(f"✅ 成功记录一笔{record_type}{amount}")
    return records

    def calculate_balance(records):
    """计算当前结余"""
    income = sum(r['amount'] for r in records if r['type'] == 'income')
    expense = sum(r['amount'] for r in records if r['type'] == 'expense')
    return income expensedef calculate_balance(records):
    """计算当前结余"""
    income = sum(r['amount'] for r in records if r['type'] == 'income')
    expense = sum(r['amount'] for r in records if r['type'] == 'expense')
    return income expense

    def delete_record(records):
    """
    删除一条记录,提供更友好的错误处理和重试机制

    参数:
    records (list): 包含记账记录的列表,每个记录是字典格式

    返回:
    list: 删除指定记录后的新列表,如果取消操作或没有记录则返回原列表

    步骤分析:
    1. 检查记录列表是否为空
    2. 显示所有记录列表(带编号)
    3. 让用户选择要删除的记录编号
    4. 验证用户输入的有效性,提供重试机会
    5. 从列表中移除选中的记录
    6. 返回更新后的记录列表
    """
    # 1. 检查记录列表是否为空
    if not records:
    print("📭 暂无记录可删除。")
    return records

    while True: # 添加循环,允许用户重试
    # 2. 显示记录列表(带编号)
    print("\\n当前账本记录:")
    print(f"{'编号':<4} {'类型':<6} {'金额':>8} {'备注'}")
    print("-" * 40)
    for i, record in enumerate(records, 1):
    print(f"{i:<4} {record['type']:<6} {record['amount']:>8.2f} {record['note']}")
    print("-" * 40)

    # 3. 让用户选择要删除的记录编号
    try:
    choice_input = input(f"\\n请输入要删除的记录编号 (1-{len(records)}),或输入 0 取消:")
    choice = int(choice_input)

    # 4. 验证用户输入
    if choice == 0:
    print("操作已取消。")
    return records
    elif 1 <= choice <= len(records):
    # 5. 从列表中移除选中的记录
    deleted_record = records.pop(choice 1)
    print(f"✅ 已删除记录:{deleted_record['type']} {deleted_record['amount']:.2f} ({deleted_record['note']})")
    return records
    else:
    print(f"❌ 编号无效,请输入 1-{len(records)} 之间的数字。")
    # 不直接返回,而是继续循环让用户重试
    print("请重新输入…")
    continue # 继续循环,让用户重新输入

    except ValueError:
    print("❌ 输入无效,请输入数字。")
    print("请重新输入…")
    continue # 继续循环,让用户重新输入

    # 调用示例和运行结果演示
    if __name__ == "__main__":
    # 模拟一些测试数据
    test_records = [
    {'type': 'income', 'amount': 5000.0, 'note': '工资收入'},
    {'type': 'expense', 'amount': 128.5, 'note': '超市购物'},
    {'type': 'expense', 'amount': 35.0, 'note': '早餐'},
    {'type': 'income', 'amount': 2000.0, 'note': '兼职收入'}
    ]

    print("=== 删除记录功能演示 ===")
    print("初始记录列表:")
    for i, record in enumerate(test_records, 1):
    print(f"{i}. {record['type']} {record['amount']:.2f} ({record['note']})")

    print("\\n— 调用 delete_record 函数 —")
    # 模拟用户输入:先输入无效编号,然后输入有效编号
    # 注意:实际运行时需要用户交互输入,这里用注释说明
    """
    运行效果示例:

    当前账本记录:
    编号 类型 金额 备注
    —————————————-
    1 income 5000.00 工资收入
    2 expense 128.50 超市购物
    3 expense 35.00 早餐
    4 income 2000.00 兼职收入
    —————————————-

    请输入要删除的记录编号 (1-4),或输入 0 取消:5
    ❌ 编号无效,请输入 1-4 之间的数字。
    请重新输入…

    当前账本记录:
    编号 类型 金额 备注
    —————————————-
    1 income 5000.00 工资收入
    2 expense 128.50 超市购物
    3 expense 35.00 早餐
    4 income 2000.00 兼职收入
    —————————————-

    请输入要删除的记录编号 (1-4),或输入 0 取消:abc
    ❌ 输入无效,请输入数字。
    请重新输入…

    当前账本记录:
    编号 类型 金额 备注
    —————————————-
    1 income 5000.00 工资收入
    2 expense 128.50 超市购物
    3 expense 35.00 早餐
    4 income 2000.00 兼职收入
    —————————————-

    请输入要删除的记录编号 (1-4),或输入 0 取消:2
    ✅ 已删除记录:expense 128.50 (超市购物)
    """

    # 实际调用示例(需要用户交互)
    # updated_records = delete_record(test_records)
    # print(f"\\n删除后的记录数量:{len(updated_records)}")
    # print("剩余记录:")
    # for i, record in enumerate(updated_records, 1):
    # print(f"{i}. {record['type']} {record['amount']:.2f} ({record['note']})")s

    注意 get_float_input 函数,它把原本散落在各处的 try-except 循环统一了起来。以后不管哪里需要输入数字,调用它就行,代码复用率极大提升。

    第三步:清爽的主程序(Interaction Layer)

    最后,我们来看看重构后的主程序。是不是感觉世界都清静了?

    def main():
    """主程序入口"""
    print("=== 欢迎使用极简记账本 V2.0 ===")
    records = load_records()

    while True:
    print("\\n1. 记收入 2. 记支出 3. 查账本 4. 退出")
    choice = input("请选择操作:")

    if choice == '1':
    records = add_record(records, 'income')
    save_records(records) # 数据变动后立即保存

    elif choice == '2':
    records = add_record(records, 'expense')
    save_records(records)

    elif choice == '3':
    if not records:
    print("📭 暂无记录。")
    else:
    print(f"\\n{'类型':<6} {'金额':>8} {'备注'}")
    print("-" * 30)
    for r in records:
    print(f"{r['type']:<6} {r['amount']:>8.2f} {r['note']}")
    print("-" * 30)
    balance = calculate_balance(records)
    print(f"当前结余:{balance:.2f}")

    elif choice == '4':
    print("👋 再见!记得理财哦。")
    break
    else:
    print("❌ 无效指令,请重新输入。")

    # 只有当本文件被直接运行时,才执行 main
    if __name__ == '__main__':
    main()

    重构带来的质变

    对比前后两个版本,你能直观地感受到差异:

  • 主程序极度精简:main 函数现在只负责流程控制(显示菜单、判断选择、调用功能),没有任何复杂的计算或文件操作逻辑。阅读代码就像读故事大纲一样顺畅。
  • 维护成本降低:如果想修改“保存格式”,只需改 save_records;如果想优化“输入体验”,只需改 get_float_input。不用担心改了一个地方,其他地方崩了。
  • 可测试性增强:你可以单独运行 calculate_balance 传入假数据来测试算法是否正确,而不必启动整个交互式程序。
  • 扩展性无限:现在如果要加一个“导出 Excel"功能,只需要新增一个 export_to_excel 函数,然后在 main 里加一个菜单选项即可,完全不会干扰现有逻辑。
  • 运行效

    重构前后的对比表格

    为了更直观地展示函数化重构带来的优势,下面从多个维度对比「重构前(面条代码)」和「重构后(函数化)」的差异:

    对比维度重构前(面条代码)重构后(函数化)优势分析
    代码行数 主循环内约80-100行,逻辑重复 主程序仅30行左右,功能函数各10-20行 代码量减少40%以上,逻辑更紧凑
    文件操作逻辑 每个功能分支都包含 open()、write()/read()、close() 统一封装为 load_records() 和 save_records() 两个函数 避免重复代码,统一错误处理,使用 with 自动管理资源
    输入验证逻辑 每个需要输入的地方都写一遍 try…except 封装为 get_float_input() 函数,一次定义多处调用 遵循DRY原则,验证逻辑统一,维护只需改一处
    功能扩展难度 需在主循环中添加新的 elif 分支,代码迅速膨胀 只需新增功能函数,在主菜单中添加一个选项 新增功能不影响现有代码,扩展成本降低80%
    可测试性 必须运行整个程序,通过交互测试特定功能 可单独导入并测试每个函数,如 calculate_balance([…]) 支持单元测试,快速验证核心逻辑的正确性
    错误处理 分散在各处,格式不统一,可能遗漏 close() 集中处理,使用统一的错误提示风格和资源管理 错误处理更健壮,避免资源泄漏
    代码可读性 所有逻辑混在一起,阅读需跳转多个分支 分层清晰:数据层、业务层、交互层分离 新人能快速理解架构,定位问题更高效
    维护成本 修改文件格式需改动多处,容易遗漏 修改存储格式只需改 save_records() 和 load_records() 维护效率提升3-5倍,降低出错风险
    数据格式 简单的逗号分隔文本,扩展性差 结构化JSON,支持嵌套数据,易于扩展字段 未来可轻松添加分类、标签、时间戳等元数据
    代码复用 几乎为零,逻辑重复编写 高复用性,如 get_float_input() 可用于所有数字输入场景 减少重复劳动,提升开发效率

    关键改进总结:

  • 关注点分离:将数据存取、业务逻辑、用户交互分层处理,每层只负责自己的职责
  • 单一职责原则:每个函数只做一件事,如 calculate_balance() 只负责计算,不负责显示
  • 高内聚低耦合:函数之间通过参数和返回值通信,减少直接依赖
  • 开闭原则:对扩展开放(新增功能容易),对修改封闭(现有功能稳定)
  • 通过这个对比表格,你可以清晰地看到函数化重构不仅仅是代码组织形式的变化,更是软件工程思维的体现。从"能跑就行"到"易于维护",这是每个程序员成长的必经之路。

    运行效果展示果展示

    重构后的记账本不仅代码结构清晰,运行效果也更加专业和友好。下面通过几张终端截图,直观展示程序的实际运行效果。

    1. 主菜单界面

    程序启动后,首先显示简洁的欢迎语和功能菜单,用户可以清晰地看到所有可用选项。

    === 欢迎使用极简记账本 V2.0 ===

    1. 记收入 2. 记支出 3. 查账本 4. 退出
    请选择操作:

    说明:这是程序的主界面,用户可以通过输入数字 1-4 来选择相应功能。界面设计简洁明了,避免了旧版中所有逻辑堆在一起的混乱感。

    2. 记录收入与支出

    选择记录功能后,程序会引导用户输入金额和备注,并给出明确的成功反馈。

    请选择操作:1
    请输入收入金额:5000
    备注(可选):工资收入
    ✅ 成功记录一笔收入:5000.0

    1. 记收入 2. 记支出 3. 查账本 4. 退出
    请选择操作:2
    请输入支出金额:128.5
    备注(可选):超市购物
    ✅ 成功记录一笔支出:128.5

    说明:

    • 输入验证:get_float_input() 函数确保用户只能输入有效数字,如果输入非数字会提示重试
    • 即时反馈:每条记录成功后都有明确的 ✅ 提示,让用户知道操作已生效
    • 数据持久化:每次记录后自动调用 save_records() 保存到文件,无需手动处理文件操作

    3. 查询账本与结余计算

    选择查账功能后,程序会以表格形式展示所有记录,并自动计算当前结余。

    请选择操作:3

    类型 金额 备注
    ——————————
    income 5000.00 工资收入
    expense 128.50 超市购物
    expense 35.00 早餐
    income 2000.00 兼职收入
    ——————————
    当前结余:6836.50

    说明:

    • 格式化输出:使用字符串格式化对齐各列,让数据更易阅读
    • 自动计算:calculate_balance() 函数自动区分收入和支出并计算结余
    • 空状态处理:如果没有记录,会显示"📭 暂无记录。"而不是报错

    4. 数据持久化验证

    程序退出后重新启动,之前记录的数据依然存在:

    === 欢迎使用极简记账本 V2.0 ===

    1. 记收入 2. 记支出 3. 查账本 4. 退出
    请选择操作:3

    类型 金额 备注
    ——————————
    income 5000.00 工资收入
    expense 128.50 超市购物
    expense 35.00 早餐
    income 2000.00 兼职收入
    ——————————
    当前结余:6836.50

    说明:load_records() 函数在程序启动时自动从 records.json 文件加载历史数据,实现了真正的数据持久化。JSON 格式相比之前的纯文本更结构化,也更容易扩展。

    重构前后的直观对比

    通过上面的运行效果,你可以感受到重构带来的实际好处:

  • 交互更友好:每一步都有明确的提示和反馈
  • 错误处理更完善:输入验证、文件损坏处理都有专门的函数负责
  • 数据展示更专业:表格化输出让账目一目了然
  • 稳定性更高:函数化的设计让每个功能模块独立,一处修改不会影响其他功能
  • 这些截图展示的不仅是程序的运行效果,更是良好代码结构带来的用户体验提升。当你运行自己重构后的代码,看到这样清晰、稳定的输出时,那种成就感会让你更加确信:函数和模块化是写出专业代码的必经之路。

    结语:像建筑师一样思考

    从“能跑就行”的脚本,到结构清晰的程序,这中间的距离就是函数与模块。

    今天的重构不仅仅是语法的升级,更是思维方式的转变。以前我们像是在泥地里盖房子,走到哪算哪,墙歪了就补一块砖;现在我们学会了画图纸、预制构件(函数)、使用标准材料(模块),先搭建好框架,再填充细节。

    当你下次面对一段超过 50 行的“面条代码”时,不妨停下来问自己:这段逻辑能不能拆成一个函数?这个重复的操作能不能封装一下?一旦你开始这样思考,你就已经不再是一个单纯的代码搬运工,而是一名真正的软件工程师了。

    试着把你自己的小项目也用这种方式重构一遍吧,那种看着代码变得井井有条的成就感,绝对比程序跑通那一刻还要强烈。

    赞(0)
    未经允许不得转载:171主机测评 » 代码越写越乱怎么办,用函数和模块把记账本重构一遍
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址