说实话,写到第41天的时候,我反而比第3天更紧张。第3天语法不熟,盯着报错挠头就行;第41天不一样,基础语法、循环、列表字典、函数这些内容都过了一遍,也该开始脱离教程,独立完成一个“能用的东西”。这一天练完,我才算把前面40天平铺直叙的知识点真正缝成了一个整体。
“上机练习第41天”可以理解成一种状态:一个人连续41天坐在电脑前动手写代码,正处在“眼睛会了、手还差点意思”和“手感刚热起来”之间的关键窗口期。这篇文章就是那一天的完整记录,包括为什么选择在41天做集成练习、练了什么、每一步怎么拆解、踩了哪些坑,以及之后的学习怎么续上。如果你正在自学编程、上培训班的实操课、或者刷完在线课程准备做第一个小项目,这篇内容应该能帮上忙。
1. 内容整体设计与思路拆解
1.1 从第1天到第40天:一条可复制的三阶段练习曲线
我把前40天分成三个阶段,每个阶段的目标完全不一样。第1到10天,重点是工具链和基础语法。装Python、配环境变量、选一个顺手到编辑器,然后反复练变量、判断、循环、函数。这个阶段每天大概写30到60行代码,不需要复杂项目,能把语法敲熟就行。第二段是第11到25天,开始碰核心数据结构与常用API,列表、字典、集合、字符串方法、文件读写、异常捕获、datetime这些内容。这里不再满足于“跑通”,而是要能独立写出小脚本,比如批量改文件名、生成随机密码、给学生成绩排序。第三段是第26到40天,重心转移到函数拆分和模块化,把一段又臭又长的脚本拆成有输入有输出的函数。
这个节奏不是拍脑袋定的,而是踩过坑总结出来的。很多自学的人第一个月特别容易沉迷“看语法”,视频一集一集刷,代码一行不写,结果到了真正要写项目时才发现,str.split() 和 str.strip() 都分不清什么时候该用哪个。上机练习的本质就是亲手调用这些API,直到形成肌肉记忆。前40天的任务一直比较碎,虽然每个功能都能跑,但数据和功能之间没有真正串联起来。第41天要做的,就是把碎片拼成完整程序。
这有点像健身里练腿的日子。单块肌群前面都单独练过,但深蹲时臀、腿、核心一起协同发力,跟单独做腿屈伸完全是两码事。写代码也一样:字典你会用,文件读写你会用,异常捕获你也会用,但让它们在一个程序里配合工作,就涉及数据结构设计、函数边界划分、异常触发时机这些问题,只有真正组合过才知道哪里会别扭。
1.2 为什么偏偏在第41天安排综合项目
“第41天”不是一个神奇数字,而是长期练习周期里的自然推进点。连续学习大概五到六周,新鲜感基本耗尽,这是个放弃的高发期。前10天练语法有正反馈,学会了新东西很有成就感;中间20天练API也还行;等到第5周,重复训练带来的边际红利开始下降,人会开始怀疑“我到底练了有没有用”。
对抗这种懈怠最好的办法,不是硬撑,而是制造一个中等难度的挑战,让大脑重新兴奋起来。第41天的综合项目恰好承担了这个角色。第40天结束的时候,我已经完成了三个互相独立的小工具,但每一个都只覆盖一个知识点。第41天选的项目需要同时用到函数、字典、文件读写、异常处理、测试验证——属于“跳一跳够得着”的难度。完成之后,整个人的练习状态会明显不一样,因为这个项目带来的成就感足以再撑起下一个阶段,而且这个项目之后还能继续迭代,天然衔接第42天以后的学习。
1.3 当天练习的目标与验收标准
开始当天练习之前,我先给自己定了几条硬指标,比起“今天要写500行”这种空目标实用得多:
- 完成一个400行以内的命令行工具,不依赖第三方库编译,安装后装满标准环境就能直接跑。
- 至少包含一次真实的文件读写,而不是把结果只打印在屏幕上。
- 至少拆出5个自定义函数,每个函数只做一件事。
- 用pytest给核心函数写至少5个有效用例。
- 全程不看着教程敲代码,只依据需求文字和自查清单独立完成。
为什么是这5条?因为可验收、可量化。我之前看过太多人写“今天学习了文件操作”,结果只是在 REPL 里调了一下 open() 和 read(),程序一关就算完了。第41天我刻意把检验标准定为“能运行、可测试、能复用”,这样练习完的成果可以放进 GitHub,以后找工作也能直接拿出来讲。
2. 核心细节解析与实操要点
2.1 项目选题:为什么第一选择是命令行记账本
第41天的项目,我选了“命令行记账本”。项目选得好不好,直接决定练习体验。这个记账本要满足:用户能新增一条记录,记录里有金额、分类、备注、时间;能按分类汇总;数据断电后不丢,保存在本地文件里。
选它的理由有四个。第一,贴近生活,正反馈强。程序写完,我当天就真的拿它记了几笔开销,有一种“工具马上能服务自己”的爽感。第二,字段清晰,只涉及金额、分类、备注、时间,不牵扯复杂的接口和外部依赖。第三,全面覆盖前面学的核心知识点,数据存储需要文件操作,分类汇总需要字典和列表遍历,交互过程需要input输入和异常处理。第四,后续扩展空间大,从命令行升级到Web版、加上图表展示,路径非常自然。
这相当于裁缝做一身简单衬衫,既要练直线车缝也要练转弯收边,练完就能穿出门,于是所有动作都有了明确意义。如果一上来就选聊天机器人、爬虫这类项目,热度是高,但前置知识缺口太大,写两天就卡死了,反而不利于坚持。
2.2 数据结构与文件格式的选择
数据存哪里?用JSON文件。很多人第一反应是CSV,因为它能用Excel打开,看着直观。JSON则是无边界的结构化数据,嵌套字段处理起来特别舒服。记账这个场景,记录本身就是“列表+字典”的组合,JSON几乎不用做额外转换。我没选SQLite,不是它不好,而是第41天这个阶段,引入数据库连接、主键约束这些概念会让练习焦点失焦。
对于数据,我用的关键设置是:
import json from pathlib import Path DATA_FILE = Path("records.json") def load_records(): if not DATA_FILE.exists(): return [] with open(DATA_FILE, "r", encoding="utf-8") as f: return json.load(f) def save_records(records): with open(DATA_FILE, "w", encoding="utf-8") as f: json.dump(records, f, ensure_ascii=False, indent=2)这里必须重点说 encoding="utf-8" 和 ensure_ascii=False 这两个参数。不写 encoding,Windows 上很容易遇到 UnicodeDecodeError 或者文件变成一坨 \u 转义,因为 Windows 默认的编码是 GBK;用了 ensure_ascii=False,中文才会以可读形式存进文件,否则全变成 \u6b3e\u9879 这种。indent=2 是纯给人类可读性准备的,不算功能,但开了它之后手动打开 records.json 排查数据时体验好很多。
关于数据导入导出,我顺手做了一个限制:文件以最简方式存储,不维护自增ID。这样做的原因是,第41天阶段还不需要数据库主键的概念。列表里每条记录靠“索引+时间戳”就行了,要坚持读懂“数据文件是可读的”这个直觉。
2.3 功能边界与函数分级
项目虽然小,我也强行按三层来组织了。数据层负责读写,业务层负责处理,交互层负责与用户对话。
- 数据层:load_records() 和 save_records(),其他代码不直接碰文件。
- 业务层:add_record()、delete_record()、summary_by_category(),不关心输入是怎么来的。
- 交互层:main() 里负责命令行选项和 input() 提示。
为什么要这样拆?因为不拆,最后很容易变成一个大函数里堆着 open、input、print、sum,看起来能跑,但一个月后自己都看不懂。单一职责原则在这个小项目里就开始练,后面写稍大的程序时你就不会不习惯了。
拆函数时我遵循一个粗标准:每个函数以动词开头,参数最多不超过3个。一条记账数据就三个核心字段,金额、分类、备注,加上时间戳,再放一个数据文件路径作为可注入参数,刚好卡在4个以内。
3. 实操过程与核心环节实现
3.1 环境准备与工程初始化
操作系统是Windows 11,Python用的是3.10,因为 3.8 以上都完全够用,不需要最新版抛头露面。编辑器选VS Code,配好Python扩展就行,不用额外装重型IDE。整个项目就一个文件加一个测试文件,不搞虚拟环境,也没用requirements.txt。
我这样操作的原因很简单:第41天上机练习的重点是逻辑闭环和工程习惯,不是部署发布。先建目录结构:
lesson41/ ├── records.py # 主程序 ├── test_records.py # 测试文件 ├── records.json # 数据文件(运行后自动生成) └── README.md # 记录练习目标与使用说明工程初始化我还做了一件事:git init 并完成了第一次提交。第一次提交只放README和空的主程序框架。版本管理这件事越早养成习惯越好。后面每实现一个功能我提交一次,这样万一写崩了还能准确回到上个可用版本。
3.2 从需求到代码的落地顺序
我不建议一上来就写完整代码,而是先把需求转成注释框架,再逐块填充。开场先在一张纸上列出单子,然后照顺序实现。
第一步实现数据层。load_records 的逻辑很简单:文件不存在时返回空列表,存在时open读取。这里有个我反复提醒自己的坑:别用“open(...,'r')”默认编码,一定要显式传 encoding="utf-8"。save_records 在所有写操作后调用。单独提这两个函数出来,是因为后面不管加多少业务功能,读写都往这两个函数收口。
第二步实现业务层。add_record 的初始版本长这样:
def add_record(records, amount, category, note=""): record = { "amount": float(amount), "category": category.strip(), "note": note.strip(), "date": datetime.now().strftime("%Y-%m-%d %H:%M:%S") } records.append(record) save_records(records) return record初始版故意没有校验金额,这是刻意的。先把主链路跑通,再补校验,比一上来就写满防御性代码更容易一次调试通过。跑通后再补上:
if amount <= 0: raise ValueError("金额必须大于0")第三步实现汇总。按分类汇总的本质是遍历所有记录,把相同类别的金额累加。用字典来实现特别自然:
def summary_by_category(records): result = {} for r in records: result[r["category"]] = result.get(r["category"], 0) + r["amount"] return result这句 result.get(r["category"], 0) 是核心,它表示“如果分类已经在字典里,就用已有值;否则从0开始加”。不写这个而用 if category in result:,代码会多两行,而且读起来乱。
第四步实现命令行交互,用最简单的 while 循环:
def main(): records = load_records() while True: cmd = input("请输入命令(add/summary/quit):") if cmd == "add": ... elif cmd == "summary": ... elif cmd == "quit": break这里我没用 argparse,因为4个命令级别的小工具用 argparse 有点杀鸡用牛刀了。但我在main函数里也留了约束:数据从 load_records 读入,每次修改后通过 save_records 写回,不让任何业务函数直接动手改文件。
3.3 单元测试与手动验证的配合
测试在这一天正式登场。写测试文件 test_records.py,用 pytest 框架。每个用例都通过参数传入数据文件路径,避免测试之间污染真实文件。最关键的一个用例是验证 add_record 与 summary_by_category 能连起来工作:
import json from pathlib import Path import pytest from records import add_record, summary_by_category, load_records def test_add_record_and_summary(tmp_path): # 使用临时目录下的文件,不触碰真实 records.json data_file = tmp_path / "test_records.json" ...测试里用 tmp_path 并不是在耍酷。如果测试直接用项目目录里的 records.json,跑完第一个用例文件里就多了几条数据,第二个用例接着读,断言结果可能就不准了。测试隔离是一个必须练的工程素养,第41天开始养成,后面写任何项目都受益。
手动验证也要认真做。写完测试后,我重新在命令行里跑了一遍完整流程:先 add 了两次“餐饮”、一次“交通”,再输入 summary,看到输出里两个分类的汇总都正确,这才算是“上机完成”了。这个手动验证环节不能省,它能捕捉到测试没有覆盖到的交互问题,比如输入时不小心敲了空格,数据是否会被单独存成一条脏记录。
4. 常见问题与排查技巧实录
4.1 Windows 下文件编码问题
第41天练习中最经典的问题,就是我前面反复提到的编码问题。在 Windows 上不写 encoding="utf-8" 直接读 JSON,大概率报UnicodeDecodeError: 'gbk' codec can't decode byte 0x98...。
排查这个问题时,一开始想不通:文件明明是程序自己生成的,怎么会读不回来?后来查日志才反应过来,save 时 json.dump 我传了 ensure_ascii=False,写进文件的是中文;load 时 open() 默认按 GBK 解码,GBK 和 UTF-8 的中文字节流对不上,必然报错。解决就一行代码:open 时传 encoding="utf-8"。
经验是:所有文件读写、网络传输、数据库连接,在编码上永远显式声明,别依赖环境默认值。默认值在不同系统、不同运行环境中不一样,靠默认值,早晚踩坑。这属于“主动为错误买单”,Bug 明明可以避免。
4.2 金额精度与用户输入的坑
用 float 存金额能跑,但严格说不够严谨。Python 的二进制浮点数对 12.5 这类小数没有精确表示,print 出来没问题,但多个小数运算后面会产生很长的尾巴。记账工具练到后面一旦做统计汇总,数据异常是迟早的事。
第41天先不引 Decimal 模块,因为基础阶段的重点是掌握流程。但我做了一步保守处理:求和之后 round(sum_value, 2),保证输出至少显示正常。等第50天左右引入 Decimal 再做严谨判断,读者到时会明白两个选择之间的区别。
其实用户输入的坑比精度更隐蔽。有人手滑输入了负数,初始版本没有校验,程序会默默把一笔“负支出”记进去,后面汇总全乱。这个经验强调一个原则:任何程序的对外入口,默认不信任输入,校验放在最靠近入口的位置。
4.3 pytest 测试相互污染
测试第一次跑全绿,第二次跑就挂,这个现象特别容易出现在新手的第一个测试文件里。原因就是测试之间共用了同一个数据文件,第一个用例写入了数据,第二个用例执行时 records.json 里已经有内容了,断言自然对不上。
解决方法是让数据文件路径可注入。也就是 load_records 和 save_records 设计成能接收一个路径参数,测试时传 tmp_path / "test.json"。我在第41天练习里专门在测试模块里重新定义了一个“测试版”的数据层方法,让每个用例都指向临时文件,才彻底摆脱了这个诡异问题。
这里留一个排查思路:出现“单次跑成功、连续跑崩溃”的现象,优先怀疑全局状态或共享文件,而不是怀疑代码真随机。99% 的情况都是上一次运行留下的残留数据影响了本次判断。
4.4 长期上机练习的几条独家经验
最后分享几条长期练下来才体味到的经验。第一,每次上机时长控制在90到120分钟,超过就走神效率骤降。第二,中断没那么恐怖,但别连续断两天,只要隔天没有回补,手感马上会退化一大截。第三,给每天定一个“最低完成量”,哪怕只写5行代码也要打开编辑器执行,这种仪式感比写多少行更重要。第四,绝对不要用“看别人代码”代替自己动手,看懂了离写对还差十万八千里,这个感受在练习中途特别深刻。
如果你也正在做类似的连续上机练习,我的建议是别苛求完美,一点点推进。第41天最大的意义在于,一个普通人连续琢磨同一件事四十多天,这件事已经不只是技能训练,而是一份连续的见证。它告诉你,把一件事变成日常,比天赋更容易带来进步。
练完这个记账本,下一个阶段就可以考虑把同样的业务逻辑用面向对象重构一遍,引入 Decimal、Click 命令行框架;等到第60天前后,再把数据接到 SQLite 或者做一个简单的 Web 展示页。每一步都是对今天这版代码的自然延伸,上机的意义也在于此——你不仅要学会写代码,更要学会把一个想法变成可以运行、可以测试、可以分享的东西。这是第41天教会我的事。