尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

12岁小学生重构Python代码:一场教科书级重构实战

12岁小学生重构Python代码:一场教科书级重构实战 如果只看新闻标题很多人会以为这又是一条“神童新闻”12岁、小学生、重构代码。这三个词放到一起确实足够吸引注意力。但真正坐下来拆开看你会发现这里面既没有天才密码也没有高不可攀的算法而是一个非常朴素的编程动作把一段能跑的代码改得更清楚、更好改。这件事之所以值得写成一篇文章是因为它离绝大多数开发者的日常太近了。很多工作了几年的程序员手上都有一批“能跑但不敢动”的代码看到一个12岁孩子愿意回头整理自己写的小程序这比任何教育口号都更能说明问题重构不是高阶开发者的大招而是可以从小养成、也必须尽早养成的工程习惯。这篇文章会以少儿编程中的一个图书角借还登记程序为例还原一次有代表性的重构全流程。我们会先看到重构前的代码长什么样然后找出其中的“坏味道”再一步一步把它改造成结构清晰的版本。整个过程中会使用 Python会涉及命名、字典映射、函数拆分、输入处理和回归验证适合刚学完语法、想让代码更专业的初学者也适合带学生和带新人的工程师。读完这篇文章你可以得到两样东西一份马上能用的代码重构入门清单以及一双判断代码好坏的基本眼光。1. 先回答一个问题重构到底在解决什么重构这个词在不同的语境里含义差别很大。放在“机房重构”这类运维场景里它指的是对整个机房或系统结构做调整放在大型前端项目里它可能是把几万行旧代码改造成新架构。但无论规模大小重构的内核是一样的在保持外部功能不变的前提下调整代码内部结构让阅读、维护和扩展变得更加容易。换句话说重构不是“我要写一个新功能”而是“代码虽然能用但我让它更好用”。“更好用”的标准并不抽象落到具体操作上就是命名是否准确、逻辑是否有重复、函数会不会太长、分支能不能简化、数据是否被组织得清楚。这些标准不需要极高的算法水平也不需要很长的工龄它首先是一种“愿意替下一个读代码的人着想”的习惯。一个刚学 Python 半年的初学者完全可以通过一次小规模重构让自己的代码从“勉强能跑”变成“结构清楚”。1.1 重构不是重写很多人第一次听到“重构”下意识会理解成“推翻重来”这其实是一个很大的误区。重构是整理不是重写是局部手术不是整体更换。一套重构做完用户看到的功能不应该有任何变化当初能通过的测试也应该继续通过。如果一边重构一边改需求那代码出了问题就说不清是结构引入的还是需求变更引入的。更有经验的团队会明确区分“重构”和“需求变更”两个任务不同时进行。这里可以做一个很生活化的类比整理房间。你收拾自己的书桌把文具放到笔筒里把常用的书放到触手可及的位置。桌面上的东西没有变多也没有变少但找东西更快了坐着也舒服了。重构代码其实就是这样一个过程数据还是那些数据功能还是那些功能但组织方式变得更有条理后续改动时不会牵一发而动全身。1.2 为什么“12岁小学生也能重构”并不夸张看到“12岁小学生重构代码”很多人的第一反应是怀疑这么小能重构什么其实重构的入门能力并不依赖高深知识它依赖的是三个非常基础的东西读懂自己的代码、发现重复、愿意把逻辑拆开。这些能力在小学五六年级的编程兴趣班上完全可以培养。案例里的孩子并不是在维护几十万行的企业系统他面对的是一个班级图书角的借还登记程序代码量很小但已经有了典型的“坏味道”。他能做这件事说明老师在教编程时没有只盯着“跑出结果”而是带着孩子回头看代码本身。把视野放大一些重构也是最近行业里非常高的话题。有头部公司分享过用 AI 辅助重构数万行前端代码的经验也有工程师在“机房重构”的背景下讨论如何让历史系统迁移后保持稳定。这些新闻的共同点在于代码规模越大、生命周期越长重构的价值就越明显。而小项目里的重构恰好是让人理解这个概念的最佳起点。一个 12 岁孩子的重构不会改变行业但它能说明一个道理重构这件事和代码规模无关和思维习惯有关。1.3 这篇文章适合谁读如果你是刚学完 Python 基础语法、正在做课程设计或小项目的初学者这篇文章可以帮你把代码从“能跑”提升到“能看、能改、能交作业”。如果你是已经工作了几年、手上积攒了大量历史脚本的工程师这篇文章可以当作一次系统化的思路梳理你不需要按文章代码照抄只需要把里面的“识别坏味道、拆函数、换数据结构、做回归验证”这套动作迁移到你实际的项目里。如果你平时需要带新人、做代码评审或教编程这篇文章里的示例也可以直接拿来当教学素材用来解释“为什么这段代码要重构”比空讲原则要有效得多。2. 原始项目一段“能跑但不敢动”的代码我们先用一个场景把问题立住。假设小学编程兴趣小组里老师布置了一个真实任务班级图书角需要登记每本书的借出和归还情况。需求很简单只有三个操作借书、还书、查看所有书的状态。刚开始学习不久的孩子用最直接的方式实现了这个功能代码确实能运行班级里也在正常使用。以下是重构前的原始版本为了便于展示这里放在一个文件里# 文件路径book_old.py books [西游记, 三体, 哈利波特] book_owner {西游记: , 三体: , 哈利波特: } print(图书角借还系统) print(1. 借书) print(2. 还书) print(3. 查看所有书) cmd input(请输入操作) if cmd 1: name input(请输入书名) if name in books: if book_owner[name] : person input(请输入借书人) book_owner[name] person print(借书成功) else: print(这本书已经被借走) else: print(没有这本书) elif cmd 2: name input(请输入书名) if name in books: if book_owner[name] ! : person input(请输入还书人) if book_owner[name] person: book_owner[name] print(还书成功) else: print(借书人不对) else: print(这本书没有被借走) else: print(没有这本书) elif cmd 3: for name in books: if book_owner[name] : print(name 在库) else: print(name 被 book_owner[name] 借走) else: print(输入不合法)这段代码在功能上是完整的。借书时会检查书是否存在、是否已被借走还书时会核对借书人信息查看列表也能正确输出。对于一个初学编程的孩子来说能写出这种程度已经说明基本语法掌握得不错。但稍微熟悉工程实践的读者应该已经感觉到了这段代码虽然能跑却隐藏着不少让后续维护头痛的问题。首先books和book_owner是两份独立的数据一个用列表一个用字典它们之间依靠“书名是否一致”这个隐性约定来保持同步。一旦书名出现大小写差异、空格或别名两边的数据就很容易对不上。其次整个交互逻辑全放在一个巨型if-elif分支里代码重复很多尤其是“查找书是否存在”“判断书是否被借走”这两段逻辑在借书和还书流程中几乎是一模一样地贴了两遍。读代码的人要花不少精力才能理清每个分支到底在做什么。这还只是一个人维护的小项目。如果让这个程序继续长大比如加入“预约”“逾期”“多本书同时借阅”等功能再用这种写法堆下去代码的复杂度会迅速失控。到那时候修改任何一个小功能都可能牵动整段逻辑。所以这次重构的核心任务非常明确在不改变既有功能的前提下把代码重新组织成一个更容易扩展、更容易测试的结构。3. 重构前先识别代码的“坏味道”拿到一段待重构的代码第一件事不是立即动手改而是先做“代码诊断”。就像医生看病一样得先找到病灶在哪里才知道从哪里下刀。这里说的“坏味道”是一个编程术语它指的并不是错误而是一些让代码难以阅读、难以修改、难以扩展的结构性特征。坏味道不会直接导致程序崩掉但它们会在未来每一次改动时悄悄增加成本。3.1 一眼就能看出的坏味道下面这张表总结了这段代码里最明显的几类问题以及它们对应的改进方向坏味道具体表现重构方向重复代码借书和还书都写了“查找书是否存在”“判断借阅状态”把重复逻辑抽成公共函数长 if-elif 分支三个操作全部堆在主流程里越加越难读用字典映射把操作码和函数对应起来数据结构割裂书名列表和借阅人字典分开维护靠约定同步定义统一的数据模型比如 Book 对象命名模糊cmd、name、person不体现具体含义改成command、title、borrower输入无处理用户输入前后可能带空格导致查找失败用strip()统一清理输入逻辑与界面耦合业务判断和print输出混在一起不方便测试让业务函数返回结果字符串由调用方决定如何输出如果只看表面很容易以为这些只是“风格问题”不影响运行。但在真实项目中风格问题会积累成架构问题。重复代码意味着同一处业务规则分散在多个地方改的时候很容易漏掉一处长分支意味着新增一个功能要在大段逻辑里小心寻找插入点数据结构的割裂则意味着你无法保证两处数据永远一致。这些问题是“能跑”的代码走向“难维护”的代码的第一步。3.2 为什么要专门做代码诊断很多初学者拿到旧代码会直接开始重写结果经常是越改越乱。先诊断再动手的好处是你明确了哪些部分需要保留、哪些部分需要抽取、哪些部分可以删除。代码诊断让你把重构从“凭感觉改”变成“按清单改”这在小项目里看不出多大区别但在真实项目里是防止引入新 bug 的第一道防线。在这个案例里诊断的结论很清晰数据模型需要重新设计重复判断需要抽成函数分支结构需要简化入口流程需要整理。确定这四件事之后接下来的每一步就都有了明确目标。4. “究竟写了啥”一次典型重构的完整步骤现在进入这篇文章最核心的部分那个 12 岁孩子究竟写了什么如果只看结果他其实没有增加任何新功能也没有写出更高级的算法。他做的是四个动作把书定义成结构更清晰的数据对象、把重复的判断抽成函数、用字典映射替代if-elif分支、统一输入处理和入口流程。下面我们一步一步拆开看。4.1 第一步把“书”定义成清晰的数据结构原始代码里一本书的信息被拆成了两个独立变量books列表负责记录书名book_owner字典负责记录借阅人。这种写法在数据量小的时候没什么问题但它把“书名”和“借阅人”这两件本来应该放在一起的数据拆开了。重构的第一步就是给“书”定义一个结构让每一本书自带书名和当前借阅人# 文件路径book_refactored.py片段 from dataclasses import dataclass dataclass class Book: title: str borrower: str 这段代码用 Python 标准库中的dataclass定义了一个Book类。title表示书名borrower表示当前借书人默认是空字符串代表这本书没有被借走。对于初学者来说这段代码第一次接触可能会觉得有点抽象但它解决的问题其实很朴素原来需要两个变量互相配合才能表示一本书现在一个Book对象自己就能表达清楚。这个动作属于重构里的“梳理数据模型”。后续如果想要给书增加编号、出版社、借阅次数等字段只需要在Book类里增加属性即可不用再去同步维护一个列表和一个字典。数据结构清楚了上层逻辑才能跟着清楚。4.2 第二步把重复判断抽成函数原始代码里“查找一本书是否存在”和“判断这本书的状态”这两段逻辑在借书流程和还书流程里写了两遍。重构时把这些重复逻辑抽成一个公共函数find_book再让借书和还书操作复用同一个查找结果。这样做最大的好处是以后如果要修改查找规则比如忽略大小写、支持模糊搜索只需要改一个函数而不需要同时改两个分支。# 文件路径book_refactored.py片段 def find_book(books, title): for book in books: if book.title title: return book return None def borrow_book(books, title, person): book find_book(books, title) if book is None: return 图书不存在 if book.borrower ! : return 图书已被借走 book.borrower person return 借书成功 def return_book(books, title, person): book find_book(books, title) if book is None: return 图书不存在 if book.borrower : return 图书未被借走 if book.borrower ! person: return 借书人信息不匹配 book.borrower return 还书成功这里有一个对初学者非常重要的变化原来的代码里业务判断和print输出是混在一起的。重构之后borrow_book和return_book不再负责直接打印内容而是返回一个表示结果的字符串。这意味着你可以像调用普通函数一样测试它们也可以用它们来构建图形界面或者网页接口而不用修改任何业务逻辑。也许你会问为什么要让函数返回结果而不是在函数里直接print因为在未来的开发中显示层是很容易变化的。同一个业务逻辑今天可能在命令行里打印明天可能显示在网页上后天可能变成一个给程序调用接口。如果把print写死在业务函数里这个函数就只能在命令行场景下使用。重构后业务函数只负责“做判断、改状态、返回结果”输出方式由调用方决定。这种设计思路叫做“关注点分离”是工程化代码的一个重要标志。4.3 第三步用字典映射替代 if-elif原始代码的核心问题之一是用一个长大的if-elif分支来处理三种操作。重构后每种操作被封装成独立的处理函数然后用一个字典把操作码和函数对应起来。这样做的好处是新增一个功能时不需要在if-elif链里寻找插入位置只需要增加一个函数和一行映射即可。# 文件路径book_refactored.py片段 def handle_borrow(books): title input(请输入书名).strip() person input(请输入借书人).strip() result borrow_book(books, title, person) print(result) def handle_return(books): title input(请输入书名).strip() person input(请输入还书人).strip() result return_book(books, title, person) print(result) def handle_list(books): list_books(books) ACTIONS { 1: handle_borrow, 2: handle_return, 3: handle_list, }这段代码非常短但它把“用户输入什么”和“程序执行什么”这两个问题彻底解耦了。在这个案例中字典映射是孩子重构时最有代表性的一步因为它背后的思想在大型系统里同样成立用配置和映射代替层层堆叠的条件判断让代码的扩展点变得清晰。假如明天要加入“预约”功能只需要写一个handle_reserve函数然后在ACTIONS字典里加一行4: handle_reserve。主流程不需要改动其他功能也不会被影响。如果没有这次重构你需要在原来的if-elif链里再插一个小分支而且越到后面插入位置越难找。4.4 第四步统一输入处理写一个干净的入口最后一个动作是统一处理用户输入并写出一个清晰的main入口。原始代码在多个分支里反复调用input而且没有对输入做任何清理。重构后所有input都调用.strip()去掉首尾空格避免用户随手打出空格导致程序判断“图书不存在”。这种看似不起眼的处理在实际使用中非常影响体验也是代码健壮性的一部分。# 文件路径book_refactored.py片段 def main(): books [Book(西游记), Book(三体), Book(哈利波特)] print(图书角借还系统已启动) while True: print(\n1. 借书) print(2. 还书) print(3. 查看所有书) print(0. 退出) cmd input(请输入操作).strip() if cmd 0: break handler ACTIONS.get(cmd) if handler is None: print(输入不合法请重新输入) continue handler(books) if __name__ __main__: main()这里有两个值得注意的细节。第一使用ACTIONS.get(cmd)而不是ACTIONS[cmd]这样当用户输入一个不存在的操作码时程序不会抛出KeyError而是返回None走统一的错误提示分支。第二用if __name__ __main__:保护main()调用使得这个文件在被其他模块导入时不会自动启动程序。这个习惯对初学者来说可能有点“多余”但在工程中它意味着可以安全地导入模块中的函数做测试。5. 重构后的完整代码示例把上面的步骤组合起来就得到了完整的重构版本。这一版代码在功能上和原始版本保持一致但结构变得清晰很多。以下是可以直接运行的完整文件5.1 完整代码# 文件路径book_refactored.py from dataclasses import dataclass dataclass class Book: title: str borrower: str def find_book(books, title): for book in books: if book.title title: return book return None def borrow_book(books, title, person): book find_book(books, title) if book is None: return 图书不存在 if book.borrower ! : return 图书已被借走 book.borrower person return 借书成功 def return_book(books, title, person): book find_book(books, title) if book is None: return 图书不存在 if book.borrower : return 图书未被借走 if book.borrower ! person: return 借书人信息不匹配 book.borrower return 还书成功 def list_books(books): if not books: print(书库为空) return for book in books: status f被 {book.borrower} 借走 if book.borrower else 在库 print(f{book.title}{status}) def handle_borrow(books): title input(请输入书名).strip() person input(请输入借书人).strip() result borrow_book(books, title, person) print(result) def handle_return(books): title input(请输入书名).strip() person input(请输入还书人).strip() result return_book(books, title, person) print(result) def handle_list(books): list_books(books) ACTIONS { 1: handle_borrow, 2: handle_return, 3: handle_list, } def main(): books [Book(西游记), Book(三体), Book(哈利波特)] print(图书角借还系统已启动) while True: print(\n1. 借书) print(2. 还书) print(3. 查看所有书) print(0. 退出) cmd input(请输入操作).strip() if cmd 0: break handler ACTIONS.get(cmd) if handler is None: print(输入不合法请重新输入) continue handler(books) if __name__ __main__: main()5.2 关键逻辑说明这段完整代码表面上比原始版本多了十几个函数代码行数也增加了但它的可维护性明显更强。首先数据模型变得独立Book类把一本书的所有信息放在一起即使以后增加新的字段比如“ ISBN ”或者“累计借阅次数”也不会影响现有逻辑。其次核心业务逻辑与交互逻辑分离find_book、borrow_book、return_book是纯逻辑函数它们不依赖input和print因此可以单独测试也可以在未来被图形界面接口复用。ACTIONS字典是这段代码里最关键的调度中心。它把用户输入的操作码映射到对应的处理函数替代了原来的长if-elif。每次循环从用户那里拿到一个命令先判断是否为退出命令如果不是再从字典里拿处理函数。如果字典里没有这个命令则提示用户重新输入。这样的流程让主函数变得极短所有控制逻辑一目了然。还要注意list_books函数里的一个细节它先判断书库是否为空然后使用了一个简单的条件表达式来生成状态文本。这种写法比原来的if-else更紧凑但它仍然保持了可读性。对于初学者来说读这种代码时如果觉得不习惯完全可以把它改回原来的if-else写法这并不影响功能。重构的目标是让代码更适合当前的维护者阅读而不是一味追求“看起来高级”。6. 怎么验证重构没有改坏功能重构做完了代码也变得好看了但真正关键的问题还没解决你怎么知道这次重构没有把原来的功能改坏这可能是初学者最容易忽略的一步。很多人改完代码发现能跑就认为大功告成但真正的验证应该是把原始版本支持的所有操作场景全部在重构后的版本里重新执行一遍并确认结果一致。6.1 手动回归验证最直接的方式是运行程序手动走一遍完整流程。启动命令如下python book_refactored.py预期看到类似这样的输出图书角借还系统已启动 1. 借书 2. 还书 3. 查看所有书 0. 退出 请输入操作3 西游记在库 三体在库 哈利波特在库然后逐步验证各种场景。为了不遗漏建议对照下面这张回归验证清单操作操作预期结果借一本不存在的书输出“图书不存在”借一本在库的书输出“借书成功”再查询时显示已借重复借同一本书输出“图书已被借走”用错误的人名还书输出“借书人信息不匹配”正确还书输出“还书成功”图书回到在库还一本没有被借走的书输出“图书未被借走”输入非法命令比如 9提示“输入不合法请重新输入”输入 0 退出程序正常结束这份清单实际上就是原始版本已经支持的全部功能点。如果每一项都符合预期那这次重构在功能上就没有破坏任何东西。这也说明重构前把功能点梳理成清单是非常有用的准备工作它既是你的测试计划也是你向别人证明“重构没有白做”的依据。6.2 用最小自动化测试兜底如果只靠手动测试每次重构都要重新输入一遍效率很低而且容易漏掉某个极端场景。更工程化的做法是写一个最小自动化测试把核心业务函数放在assert里跑一遍。以这个项目为例可以单独写一个测试文件# 文件路径test_book.py from book_refactored import Book, borrow_book, return_book def test_borrow_and_return(): books [Book(三体)] assert borrow_book(books, 三体, 小红) 借书成功 assert borrow_book(books, 三体, 小明) 图书已被借走 assert return_book(books, 三体, 小明) 借书人信息不匹配 assert return_book(books, 三体, 小红) 还书成功 assert return_book(books, 三体, 小红) 图书未被借走执行测试可以直接运行python -m pytest test_book.py如果你的环境里没有安装pytest也可以用最简单的断言方式python test_book.py只要测试文件里没有抛出AssertionError就说明这些核心功能仍然正常。这个测试文件虽然很小但它代表了一种非常重要的工程习惯在做任何可能影响现有逻辑的修改之前先让测试把行为固定住。有了测试兜底后面再做更大的重构时才敢放开手脚。6.3 如何判断重构成功判断一次重构是否成功不只在于程序能不能跑还在于代码是否真的变得更容易修改。可以从三个角度自我检查第一新增一个功能时需要改动的代码位置是否清晰第二函数是否能在不依赖print和input的情况下被单独测试第三如果换一个人来看代码他能否在更短的时间内理解整个流程。这三个问题如果答案都是肯定的那这次重构就真正起到了效果。7. 常见问题与排查思路初学者在做重构时经常会遇到一些看起来莫名其妙的问题。下面这张表整理了这段示例代码中最可能出现的问题以及对应的排查思路问题现象可能原因排查方式解决方案运行代码后直接退出没有显示菜单main()没有被调用检查文件末尾是否有if __name__ __main__: main()在文件末尾补充调用借书时输入的汉字前后有空格导致找不到书用户输入没有做.strip()在输入后打印书名观察有没有多余空格所有用户输入统一调用.strip()导入模块时程序自动启动了菜单启动逻辑写在模块顶层检查是否有无条件调用main()把启动逻辑放到if __name__ __main__:里使用ACTIONS[cmd]时报KeyError用户输入了不存在的操作码查看报错位置的栈信息改用ACTIONS.get(cmd)统一处理未知命令调用函数时报“缺少参数”错误处理函数的签名和调用方式不一致检查ACTIONS里每个函数的参数列表确保所有处理函数都声明books参数还书时显示“借书人信息不匹配”借书人和还书人姓名不一致打印出book.borrower和用户输入对比检查输入是否有多余空格或确认操作是否确实由同一个同学完成重构后代码行数变多了反而觉得更复杂只看到行数增加没有看到结构改善对比新旧代码的分支层级和重复程度行数不是关键指标重点是重复是否减少、函数是否独立如果你在运行过程中遇到TypeError大概率是函数签名和调用点没有对齐。比如你给handle_borrow定义了两个参数但在ACTIONS字典里却写成了一个参数调用时就会报错。排查方法是查看报错信息中提示的“缺少参数”或“多余参数”然后回到函数定义处逐一核对。另一个常见的误解是重构后的代码一定比原来的短。其实在很多案例中重构后的代码行数反而增加了因为原本隐式存在的逻辑被明确写成了函数。这不代表重构失败重复代码的消除、职责的拆分、结构的清晰这些才是更重要的指标。判断重构是否成功要关注的是未来改代码时省下的时间而不是当前省下的字符数。8. 从孩子身上学到的工程建议一个 12 岁孩子的重构当然无法和大型系统的架构升级相提并论但这件事折射出的方法论是通用的。如果你能理解并应用下面这些建议你的代码质量会明显提高而且会更早地体会到“维护代码”和“写完代码”之间的区别。8.1 一套可以复制的重构流程把这次小重构抽象成流程大致可以分为六步第一步先明确现有功能点最好写成清单第二步识别代码中的坏味道比如重复、长分支、命名不清第三步优先调整数据结构让数据模型稳定下来第四步抽取公共逻辑为函数消除重复第五步简化分支结构用映射或别的数据结构替代冗长的条件判断第六步运行回归验证确认功能没有被破坏。这套流程并不只适用于 Python 小项目。在 Java 项目里你可能会用MapString, Function替代命令链在 Vue 项目里你可能会把散落在组件中的重复逻辑抽取成公共函数在数据库脚本里你可能会把重复的子查询改成视图。结构变了原则不变一次只改一件事改完立刻验证。在实际团队协作中我特别推荐结合 Git 来执行重构。重构前先提交一个“重构前”的节点然后为重构单独创建分支每完成一步就同步一次提交。这样即使中间出了问题也可以随时回退到上一个稳定版本。git init git add . git commit -m 重构前功能可用的初始版本 git checkout -b refactor-book这条命令串就是一个小型团队的标准操作。它确保你的调试验证过程中任何一步都不会被永久丢失也让代码评审者可以清晰地看到每次提交只改了一个东西功能没有被意外影响。8.2 安全边界重构生产代码前必须做的事这里要特别提醒一句
返回列表