编程练习这件事我见过太多人卡在同一个地方语法都懂小例子都会一碰到“把功能串起来”就不知道从哪里下手。3个综合练习题目就是专门用来破这个局的。它不是一个知识点配一个demo而是把文件操作、数据解析、交互逻辑、接口设计这些零散内容揉到三个完整可运行的小项目里让练习者在真实场景里把“学过的东西”变成“会用的技能”。这篇文章我会按难度递进逐个拆解命令行记账本、浏览器待办事项应用、轻量级待办事项API。每个题目都讲清楚为什么这么设计、核心怎么写、哪些坑我帮你先踩过按这个顺序练下来你会明显感觉到自己的代码组织能力不一样了。1. 练习题目设计的整体思路1.1 为什么一定要选“能完整跑起来”的题目很多人练编程喜欢挑那种“算法题”比如反转链表、排序数组。不是说算法没用而是如果只练算法你会发现真的要去写一个能交给用户的东西时还是两眼一抹黑。综合练习题目和算法题最大的区别在于它逼着你考虑数据怎么存、界面怎么交互、出错怎么办、以后怎么扩展。这才是真实开发里消耗精力最多的地方。命令行记账本看起来很简单但你要处理文件读写、日期格式、金额校验、统计逻辑待办事项应用看似只是个列表但你要考虑状态同步、DOM更新、浏览器存储待办API看似只要几个接口但你要设计数据模型、异常处理、测试方式。这些恰恰是从“会写代码”到“能交付代码”之间的关键差距。这里有个我一直坚持的看法练综合项目代码量并不是第一目标把每个功能模块“咬合”在一起的能力才是。你写一个函数和写一个系统区别不在函数本身而在函数之间的边界和调用关系。这三个题目正好覆盖了三种最常见的程序形态本地命令行工具、纯前端交互页面、后端接口服务。练完这一个组合你对“程序从输入到输出、从存储到展示”的完整链路会有很具体的体感而不是停留在抽象概念上。1.2 三个题目的难度阶梯与能力对照三个题目不是随便凑的我按难度和依赖方向做了仔细划分。命令行记账本只用Python标准库不装任何第三方包入门成本最低适合刚学完基础语法、想练文件读写的人。待办事项应用只用HTML、CSS、JavaScript不碰框架练的是纯浏览器环境下的DOM操作与本地存储。待办API用FastAPI配合Pydantic会引入框架思维、REST风格、接口测试这些概念对想往后端方向发展的人特别有帮助。我把三个题目的核心能力对应关系放在下面你可以对照自己的情况选择练习重点题目主要编程语言/技术核心能力训练点适合基础命令行记账本Python 标准库函数拆分、JSON读写、命令行交互、异常处理掌握基础语法即可待办事项应用JavaScript 浏览器APIDOM操作、事件绑定、localStorage、状态同步学过JS基本语法待办事项APIPython FastAPI接口设计、数据校验、HTTP方法、自动化测试了解Python、想入门后端这样安排还有一个好处三个项目之间不需要互相依赖你单独练哪一个都可以。但如果按顺序练前面的基本功会在后面直接复用。比如你在记账本里练过的JSON序列化和反序列化到了API项目里就是请求体和响应模型的雏形你在待办事项应用里练过的“新增数据、刷新视图”逻辑到了API项目里就变成了Create和Read两个接口。技能之间是互相借力的。2. 第一题命令行记账本2.1 需求定义与功能拆分我见过很多记账本教程上来就让你写GUI或者网页版这其实是把简单问题复杂化了。命令行记账本的核心价值不在界面而在于“记录”和“统计”这两个动作。我给它的需求定得很朴素支持记录一笔收入或支出支持按类别查询支持按月汇总数据持久化保存到本地文件不依赖第三方库。为了让项目边界清晰我把功能拆成三个模块数据层负责读文件、写文件业务层负责新增记录、筛选、汇总交互层负责读取用户输入并调用业务层。具体来说数据层用一个JSON文件当作“数据库”字段结构我建议固定为amount、category、note、date四项。amount统一用浮点数正数代表收入负数代表支出date用YYYY-MM-DD的字符串保存方便后续比较和按月聚合。业务层里最核心的是add_transaction和monthly_summary两个函数前者把一条记录追加到列表里后者遍历所有记录按年月分组求和。这样拆完以后你会发现哪怕后面要升级成SQLite存储或者Web界面数据层和业务层都可以复用只需要替换交互层。这就是模块化的价值。很多新手写这类功能时会习惯性地把所有代码堆在主循环里短时间能跑但一旦要加统计功能主循环会膨胀得没法维护。我建议从一开始就养成“函数只做一件事”的习惯每个函数控制在20行以内主流程只负责调度。2.2 核心实现JSON文件读写与命令行交互数据层我用json模块实现但有一点值得注意json.dump默认会把数据压成一行中文也会被转义成\uXXXX这对排查数据问题很不友好。所以我保存时会强制加两个参数ensure_asciiFalse让中文正常显示indent2让数据按层级缩进。这样你打开JSON文件就能直接看到有没有脏数据。import json from datetime import datetime DATA_FILE ledger.json def load_data(): try: with open(DATA_FILE, r, encodingutf-8) as f: return json.load(f) except FileNotFoundError: return {transactions: []} def save_data(data): with open(DATA_FILE, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2) def add_transaction(data, amount, category, note): data[transactions].append({ amount: float(amount), category: category, note: note, date: datetime.now().strftime(%Y-%m-%d) })这里我故意用try...except FileNotFoundError而不是直接判断文件是否存在是因为在并发或异常场景下“先判断后读取”容易出现竞态问题。对新手来说直接捕获异常更省心逻辑上也更贴近“文件不存在就当空数据用”这个语义。菜单部分我用一个while True死循环包住input()用户输入数字选择功能输入q退出。注意input()拿到的永远是字符串转float时要做校验否则用户输入abc程序会直接崩溃。def main(): data load_data() while True: print(\n1. 记一笔 2. 本月汇总 3. 查看全部 4. 退出) choice input(请选择).strip() if choice 1: try: amount float(input(金额收入正数支出负数)) category input(类别).strip() note input(备注可跳过).strip() add_transaction(data, amount, category, note) save_data(data) print(已保存) except ValueError: print(金额格式不正确本次操作已取消) elif choice 2: monthly_summary(data) elif choice 3: show_all(data) elif choice 4: break else: print(无效选项请重新输入)执行上面的代码后你会得到一个可以反复使用的记账工具。每次运行都会读取ledger.json所有记录持久保存。月汇总函数我建议按date[:7]取字符串前7位作为“年-月”分组键这个技巧比用datetime.strptime拆解更直接也避免时区等隐性坑。你还可以在汇总时顺手统计一下总收入、总支出和结余给这个练习多增加一点“可用性”。2.3 这个练习里最容易被忽略的细节第一个坑是路径问题。很多人把文件名直接写成ledger.json程序跑起来没问题但当你换一个目录启动脚本时文件会在新目录下重新创建数据就“丢”了。我建议用pathlib.Path(__file__).parent拼出脚本所在目录让数据文件始终和脚本在一起。这个细节虽然小但能帮你理解相对路径和绝对路径的区别。你可以这样改from pathlib import Path BASE_DIR Path(__file__).parent DATA_FILE BASE_DIR / ledger.json第二个坑是金额精度。记账本里我用float但如果你要算利息或者做更严谨的财务统计浮点数会产生类似0.1 0.2 0.30000000000000004的问题。练完基础版之后可以把金额改成用Decimal或者直接用整数分来存储这是一个很好的进阶方向。用整数分要注意的是用户输入12.34时你要换算成1234展示时再除以100这样所有的加减运算都是整数运算不会有精度损失。第三个坑是交互层的校验。我在上面代码里处理了ValueError但用户还可能输入空字符串、超长备注、不存在菜单编号。实用主义一点先兜住最会发生的崩溃不要追求把所有输入都校验一遍。真实项目里也是如此优先保证主流程稳定再把边界问题逐个补上。如果后续扩展成多用户版本你还需要考虑并发写入同一个文件的问题那时候可以引入threading.Lock或改用SQLite。3. 第二题浏览器待办事项应用3.1 页面结构与核心状态设计待办事项应用是前端练习里最经典的一道题因为它麻雀虽小五脏俱全有表单输入、有列表渲染、有状态切换、有本地存储翻来覆去能玩出各种花样。我的要求是不用任何框架只用原生JavaScript实现。这样做的好处是你能真正理解document、localStorage、事件冒泡这些底层机制而不是被框架抹平。很多同学在写完这个项目之后再去看React的useState会有一种恍然大悟的感觉。页面结构我建议保持极简就是一个输入框、一个添加按钮、一个任务列表再加三个筛选按钮。HTML骨架大概是这样input idtodo-input placeholder输入新任务 / button idadd-btn添加/button div button>const input document.querySelector(#todo-input); const addBtn document.querySelector(#add-btn); const list document.querySelector(#todo-list); let todos JSON.parse(localStorage.getItem(todos)) || []; let currentFilter all; function save() { localStorage.setItem(todos, JSON.stringify(todos)); } function render() { const filtered todos.filter(todo { if (currentFilter active) return !todo.completed; if (currentFilter completed) return todo.completed; return true; }); list.innerHTML ; filtered.forEach(todo { const li document.createElement(li); li.textContent todo.title; li.className todo.completed ? completed : ; li.dataset.id todo.id; list.appendChild(li); }); } function addTodo() { const title input.value.trim(); if (!title) return; todos.push({ id: Date.now(), title, completed: false }); save(); render(); input.value ; input.focus(); } addBtn.addEventListener(click, addTodo); input.addEventListener(keydown, (e) { if (e.key Enter) addTodo(); }); list.addEventListener(click, (e) { const id Number(e.target.dataset.id); const todo todos.find(t t.id id); if (!todo) return; todo.completed !todo.completed; save(); render(); });localStorage只能存字符串所以每次保存时用JSON.stringify每次读取时用JSON.parse这两个操作会贯穿整个项目。第一次做这个练习的人经常会忘记在页面刷新后把数组重新载入所以我会在开头写JSON.parse(localStorage.getItem(todos)) || []用空数组兜底。注意JSON.parse是有可能抛异常的如果之前存的数据被手动改坏了页面会白屏。进阶版本可以用try...catch包一层解析失败就重置为空数组。筛选功能我建议用一个currentFilter变量保存当前状态渲染时先根据它过滤todos再生成列表。这样代码写起来最简洁也顺带练到数组的filter方法。很多初学者会把筛选逻辑写在模板字符串里用三元表达式一层层嵌套那样可读性会非常差不如单独用filter函数处理。筛选按钮的事件可以统一绑定读取>from fastapi import FastAPI, HTTPException from pydantic import BaseModel from typing import Optional app FastAPI() class TodoCreate(BaseModel): title: str completed: bool False class Todo(TodoCreate): id: intTodoCreate是客户端传进来的结构只包含title和completedTodo是在存储时额外加上id的完整结构。把“创建时的输入”和“返回时的输出”分成两个模型是API设计里非常重要的习惯。它能让接口的约束更清楚客户端无法自己指定id服务端也不会把内部字段意外暴露出去。如果你直接把TodoCreate当响应模型用会发现响应里永远没有id前端根本不知道怎么定位资源。这就是模型拆分不彻底导致的直接问题。4.2 用FastAPI把核心接口写出来为了保持示例简单我直接用内存列表存储不接数据库。这样练的是接口逻辑而不是数据库用法。如果你已经学过SQLite或者MySQL可以把它替换成数据库操作如果没学过先把内存版跑通再逐步加持久化。内存版的缺点很明确服务一重启数据全部消失但这正好是一个天然的练习切入点——逼着你去了解SQLite。from fastapi import FastAPI, HTTPException from pydantic import BaseModel app FastAPI() class TodoCreate(BaseModel): title: str completed: bool False class Todo(TodoCreate): id: int todos [] next_id 1 app.get(/todos, response_modellist[Todo]) def list_todos(): return todos app.post(/todos, response_modelTodo, status_code201) def create_todo(payload: TodoCreate): global next_id todo Todo(idnext_id, **payload.model_dump()) todos.append(todo) next_id 1 return todo app.delete(/todos/{todo_id}, status_code204) def delete_todo(todo_id: int): for i, todo in enumerate(todos): if todo.id todo_id: todos.pop(i) return raise HTTPException(status_code404, detail任务不存在)这里有几个关键点值得展开。response_model参数指定了响应结构FastAPI会自动过滤掉不属于模型的字段也自动把Python对象序列化成JSON。status_code201表示创建成功204表示删除成功且无响应体这些状态码语义是后面写真实API时绕不开的基础功。payload.model_dump()是Pydantic v2的写法如果你用的是旧版本可能看到的是payload.dict()这两个在新旧版本里都不算错但要注意别混用。删除接口当时我故意处理了“任务不存在”的情况返回404。这个细节很多人会漏掉接口不能只考虑正常路径还要考虑异常路径。写完核心接口后我强烈建议你把uvicorn跑起来用curl或者Postman发几个请求验证重点看三种场景正常创建、删除一个不存在的id、POST时缺少title字段。第三种会得到422 Unprocessable Entity响应那是FastAPI自动完成的字段校验能让你真切感受到“框架帮你省了多少事”。uvicorn main:app --reload启动后打开http://127.0.0.1:8000/docs你可以直接在浏览器里调试接口。这时候你可能会发现调试接口比调试命令行程序更直观因为每个请求的输入输出都是透明的。这个“可见性”正是前后端分离架构的优势之一前端和后端可以并行开发只要接口约定不变。4.3 自动测试与联调技巧后端代码最怕的是“手动点一遍没问题一改就崩”。所以我建议从写接口的第一天就开始配测试。FastAPI搭配httpx和pytest可以非常轻量地做接口测试不需要启动真正的服务器from fastapi.testclient import TestClient from main import app client TestClient(app) def test_create_todo(): resp client.post(/todos, json{title: 写测试}) assert resp.status_code 201 assert resp.json()[title] 写测试 def test_delete_missing_todo(): resp client.delete(/todos/999) assert resp.status_code 404这种测试跑在内存里速度极快。一开始写测试可能觉得麻烦但等你给接口加上鉴权、分页、数据库之后测试的价值会翻倍增长。自动化测试本质上是在给你的接口“上保险”每改一次代码就跑一遍测试比手动重复验证省太多时间。再提一个我在联调时踩过的坑前端用浏览器直接访问后端接口时如果前端跑在http://localhost:8080而接口跑在http://localhost:8000跨域请求会被浏览器拦截。FastAPI里可以用CORSMiddleware统一处理加一行配置就能放开跨域限制。这个坑不是逻辑错误但没遇到过的人可能会在浏览器控制台看到报错后一脸懵。5. 常见问题与避坑实录5.1 练习中最容易翻车的四个场景我陪练过不少学员也看过很多自己提交的代码发现大部分问题其实集中在四个点上。第一个是编码问题。Python读取或写入包含中文的JSON文件时官方默认编码在不同平台上不一致Windows尤其容易报UnicodeDecodeError。解决方案就是在open()里统一指定encodingutf-8同时保证文件本身确实是UTF-8编码保存。很多新手写完代码本地跑得好好的在别人机器上一跑就乱码几乎都是编码没锁死。这个坑在记账本和API项目里都会出现尽早养成写编码参数的习惯能省掉大量排查时间。第二个是路径问题。待办事项应用里有人喜欢把localStorage理解成“数据库”但它本质上只是浏览器里的键值存储容量有限且随时可能被清掉。更关键的是如果你用file://协议直接打开HTML页面有些浏览器对localStorage的行为和服务器环境下并不完全一致。练习时建议用VS Code的Live Server或者python -m http.server起一个本地静态服务避免文件协议带来的古怪问题。你还可以打开浏览器控制台在Application面板里直接查看和清除localStorage这对调试非常有用。第三个是接口参数错误。FastAPI会根据类型注解自动解析请求但新手经常分不清路径参数和查询参数。比如/todos/{todo_id}里todo_id是路径参数/todos?completedtrue里completed是查询参数。如果混用会得到422或404。解决办法是在定义接口时始终问自己这个id是不是资源定位的一部分如果是放路径如果不是是筛选项放查询参数。还有一个相关问题是POST和PUT的区别POST通常用于新增PUT用于整体更新很多人为了省事全用POST结果接口语义越来越混乱。第四个是数据持久化的缺失。内存数组保存数据服务一重启就全丢了。练习时可以理解但最好在完成后升级成文件存储或者SQLite这会让项目的完整度上一个台阶。我见过很多人练完后只记得“接口能调通”却忘了数据到底存哪了这是非常危险的认知盲区。给API项目加上SQLite存储是个性价比很高的升级表结构很简单只有id、title、completed三个字段却能让你接触到数据库连接、参数绑定、事务这些真正的后端基本功。5.2 三个题目的边界与加分项把上面三个题目都跑通之后你容易产生一种“我都会了”的错觉。实际上每个题目都还有不少边界没有覆盖到把这些边界补上才是拉开差距的地方。记账本目前只处理了单用户单文件你可以试着让程序支持多个账本文件或者增加导出CSV的功能。CSV用csv模块就能写用Excel打开后还能做更多统计这个功能在实际使用中的价值很高。待办事项应用目前没有编辑功能你可以给每个任务加上“双击修改标题”的交互这会用到contenteditable或弹窗输入顺便练一下内联编辑的焦点管理。最难的是任务拖拽排序需要处理dragstart、dragover、drop这一组事件能把排序逻辑理顺你对浏览器事件机制的理解会上一个层次。API项目目前只有增和查你可以把更新接口PUT /todos/{id}补上顺便练习怎么在路径参数和请求体同时存在时做校验。再加上分页参数limit和offset就是一个小型真实接口了。分页看起来简单但你要考虑limit为负数、offset超过数据长度这些边界情况这些才是后端的常态。如果你还想继续深入可以试着给接口加上简单的日志记录每次请求把method、path、status_code写到一个文件里这能让你直观地看到服务的运行状态。5.3 关于代码组织和个人习惯的小建议我经常看到有人把三个题目写完就丢在文件夹里吃灰这很可惜。综合练习的意义不只是当时练一遍更是要留下给你自己“回头看”的素材。我建议你在每个项目里写一个简短的README把需求描述清楚把怎么运行写明白再把当时遇到的坑记下来。过两周再打开来看你会发现很多当时觉得理所当然的设计现在已经看出问题了——这就是成长。我给学员批改代码时最看重的不是功能实现而是函数和函数之间有没有清晰的分工。你可以在自己的代码上做一次“抽取练习”把所有超过30行的函数拆成若干小函数给每个函数写一行注释说明职责。这个过程很像整理房间刚开始觉得没必要但整理完后你很难再忍受乱糟糟的代码。每个小函数最好只做一件事参数控制在三个以内超过三个就考虑封装成一个对象或字典。我见过无数人栽在不命名上变量叫data、temp、x思路再清晰别人也看不懂。养成好命名习惯对你的长期帮助比多写几个功能大得多。提示如果你是初学者练这三个题目时不要强求一次写完。每完成一个功能就跑一下、提交一次哪怕只是“能用就好”。代码是一步步长出来的不是一下笔就完美的。6. 一个完整的实操验收清单结尾部分我给一个可以直接照着打勾的验收清单。这个清单是我在实际带项目时总结出来的用来判断“这个练习是否真的完成了”。你可以逐条对照如果所有项目都能打勾你已经远远超出了“能跑就行”的水平。记账本在无文件环境下第一次运行不会崩溃能自动初始化数据文件记账本输入非法金额只会提示错误不会中断主流程记账本可以按月汇总收入和支出结果不会出现浮点数精度问题待办事项应用刷新页面后数据不丢失任务状态还能正确显示待办事项应用支持筛选全部、未完成、已完成且筛选后列表与状态一致待办事项应用新增、切换、删除操作后localStorage里的数据同步更新API服务有独立的请求模型和响应模型客户端无法伪造idAPI服务对合法请求返回正确的状态码对非法请求返回422或404API服务可以用自动化测试覆盖创建和删除的正常路径与异常路径所有项目都有README说明运行方式和核心文件结构我在实际陪练过程中最大的体会是三个综合练习题目看起来不大但它们共同构成了一个从“会用语法”到“能写小型系统”的最小训练闭环。很多人觉得项目必须大、必须商用才有练习价值其实恰恰相反小项目里能把边界想清楚、能完整跑通一遍比半吊子做大项目有用得多。当你把最后一个API测试跑通时回头看第一个记账本你会明显看到自己理解深度的变化。如果你练完发现某个环节卡住了优先回到最基础的那一步去排查——是不是JSON没存进去是不是网络请求没发出去是不是数据没渲染出来。这三个问题覆盖了数据层、传输层、视图层你亲手把它们打通一次后面再遇到什么框架、什么工具都会觉得心里有底。