
零基础学Python:Day14!综合实战:打造个人待办事项管理器
恭喜你!坚持到了最后一天,我们已经学完了Python零基础的所有核心知识点,今天我们来做一个综合实战项目,把我们学过的所有知识点整合起来,打造一个属于你自己的个人待办事项管理器!
我们先回顾一下,十几天我们学了哪些知识点,今天都会用到:
变量、数据类型、循环、条件判断 函数定义和调用 模块导入使用 面向对象:类和对象、封装 文件操作:读写文件保存数据
🏋️ 昨天作业参考答案
昨天Day13我们学习了文件操作,留了4道作业题,这里给大家参考一下实现思路和代码:
1. 创建一个文本文件hello.txt,写入三行文字,然后读取打印出来
# 写入三行文字with open("hello.txt", "w", encoding="utf-8") as f: f.write("Hello Python\n") f.write("File Operation\n") f.write("I love Python\n")# 读取内容打印with open("hello.txt", "r", encoding="utf-8") as f: content = f.read()print(content)我们用w模式写入,再用r模式读取,with语句自动帮我们关闭文件。
2. 复制一个文本文件,把a.txt的内容复制到b.txt中
def copy_file(source, target):with open(source, "r", encoding="utf-8") as f: content = f.read()with open(target, "w", encoding="utf-8") as f: f.write(content)# 使用:把a.txt复制到b.txtcopy_file("a.txt", "b.txt")print("复制完成!")思路很简单:先读取源文件全部内容,再写入目标文件。对于大文件,可以逐行复制,避免占用太多内存:
def copy_file_big(source, target):with open(source, "r", encoding="utf-8") as f1:with open(target, "w", encoding="utf-8") as f2:for line in f1: f2.write(line)3. 统计Python源代码文件一共有多少行代码(忽略空行)
def count_code_lines(filename): count = 0with open(filename, "r", encoding="utf-8") as f:for line in f:# 去掉前后空白,如果剩下的不是空,说明不是空行if line.strip(): count += 1return countif __name__ == "__main__": filename = "test.py"# 换成你要统计的文件名 lines = count_code_lines(filename)print(f"文件{filename}一共有 {lines} 行非空代码")判断空行的核心是line.strip(),把一行前后的空白、换行都去掉之后,如果结果还是空字符串,说明这行就是空行,跳过不统计。
4. 拓展练习:把用户每次输入的内容追加写到日记文件里,用户输入exit退出
def diary_app():print("📝 简易日记程序,输入exit退出")with open("diary.txt", "a", encoding="utf-8") as f:while True: content = input("请输入日记内容:")if content.lower() == "exit":print("日记保存完成,再见!")break# 追加写入,换行 f.write(content + "\n")# 刷新缓冲区,保证内容立即写入文件 f.flush()if __name__ == "__main__": diary_app()这里我们用a追加模式打开文件,所以每次运行程序添加内容,原来的内容不会被覆盖,非常适合日记这种场景。
好了,昨天的作业就对完答案了,我们开始今天的综合实战项目吧!
我们要做的待办事项管理器,支持这些功能:
查看所有待办事项 添加新的待办事项 标记待办事项为已完成 删除待办事项 退出程序 数据保存到文件,程序退出再打开数据不丢失
📝 完整代码实现
我们用面向对象来实现,直接看完整代码,我会逐行给你讲解:
import jsonfrom pathlib import Pathclass Todo:"""单个待办事项"""def __init__(self, id, content, done=False):self.id = id# 唯一IDself.content = content # 待办内容self.done = done # 是否完成def __str__(self):# 显示待办,完成打✅,未完成打🔲 status = "✅" if self.done else "🔲"return f"{self.id}. {status} {self.content}"class TodoManager:"""待办事项管理器,管理所有待办"""def __init__(self, filename="todos.json"):self.filename = filename # 保存数据的文件self.todos = [] # 存储所有待办self.load_from_file() # 从文件加载数据def next_id(self):"""生成下一个待办ID"""if not self.todos:return 1# 最大ID + 1return max(todo.id for todo in self.todos) + 1def load_from_file(self):"""从文件加载数据""" path = Path(self.filename)if not path.exists():self.todos = []returnwith open(self.filename, "r", encoding="utf-8") as f: data = json.load(f)self.todos = [Todo(**todo) for todo in data]def save_to_file(self):"""保存数据到文件"""# 把对象转成字典才能序列化 data = [ {"id": todo.id, "content": todo.content, "done": todo.done}for todo in self.todos ]with open(self.filename, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2)def add_todo(self, content):"""添加新待办""" todo = Todo(self.next_id(), content)self.todos.append(todo)self.save_to_file()print(f"✅ 添加待办成功:{content}")def list_todos(self):"""列出所有待办"""if not self.todos:print("📝 你还没有任何待办事项,快去添加吧~")returnprint("\n📝 你的待办事项:")for todo in self.todos:print(todo)print()def mark_done(self, todo_id):"""标记待办为已完成"""for todo in self.todos:if todo.id == todo_id: todo.done = Trueself.save_to_file()print(f"🎉 标记已完成:{todo.content}")returnprint(f"❌ 没有找到ID为{todo_id}的待办")def delete_todo(self, todo_id):"""删除待办"""for i, todo in enumerate(self.todos):if todo.id == todo_id: deleted = self.todos.pop(i)self.save_to_file()print(f"🗑️ 删除成功:{deleted.content}")returnprint(f"❌ 没有找到ID为{todo_id}的待办")def run(self):"""运行程序,显示菜单"""print("🎉 欢迎使用个人待办事项管理器!")while True:print("\n===== 菜单 =====")print("1. 查看所有待办")print("2. 添加新待办")print("3. 标记已完成")print("4. 删除待办")print("5. 退出程序") choice = input("请输入你的选择(1-5): ")if choice == "1":self.list_todos()elif choice == "2": content = input("请输入待办内容: ")self.add_todo(content)elif choice == "3":try: todo_id = int(input("请输入要标记的待办ID: "))self.mark_done(todo_id)except ValueError:print("❌ 请输入合法的数字ID")elif choice == "4":try: todo_id = int(input("请输入要删除的待办ID: "))self.delete_todo(todo_id)except ValueError:print("❌ 请输入合法的数字ID")elif choice == "5":print("👋 再见,下次再用!")breakelse:print("❌ 输入不合法,请输入1-5之间的数字")if __name__ == "__main__": manager = TodoManager() manager.run()👀 代码讲解
我们来拆分讲解一下这段代码都用到了哪些知识点,每一个细节都给你讲透:
1. 面向对象设计与封装思想
我们拆分了两个类:
Todo:负责单个待办事项的数据存储和字符串显示TodoManager:负责管理所有待办,处理增删改查逻辑和数据持久化
这就是面向对象的封装思想:
每个类只负责自己单一职责,代码结构清晰,易于维护 将数据和操作数据的方法绑定在一起,符合真实世界的逻辑 降低代码耦合,修改一个类不会轻易影响另一个类
如果我们不用面向对象,所有逻辑都写在全局,代码会非常混乱,很难扩展功能。
2. 魔术方法__str__的作用
Todo类里我们重写了__str__方法:
def __str__(self): status = "✅" if self.done else "🔲"return f"{self.id}. {status} {self.content}"当我们 print(todo)的时候,Python会自动调用对象的__str__方法,返回我们自定义的友好显示格式如果不重写,默认会输出类似 <__main__.Todo object at 0x00000123>这样的对象内存地址,对用户不友好这就是面向对象多态的体现,不同类可以有自己不同的 __str__实现
3. 列表推导式的简洁写法
加载数据的时候我们用到了这个写法:
self.todos = [Todo(**todo) for todo in data]这就是列表推导式,是Python里非常简洁的创建列表的写法,等价于下面这段循环:
self.todos = []for todo in data:self.todos.append(Todo(**todo))列表推导式比普通循环更简洁,运行效率也更高,适合这种从一个序列快速转换生成新列表的场景。除了列表推导式,Python还支持字典推导式和集合推导式,用法类似。
4. 字典解包**todo是什么?
上面的Todo(**todo)里,**是字典解包操作:
我们从JSON读取出来的每个 todo是一个字典,格式是{"id": 1, "content": "xxx", "done": false}**todo会把字典的键值对自动展开成关键字参数传给Todo的构造函数等价于写 Todo(id=todo["id"], content=todo["content"], done=todo["done"])如果字典的key和构造函数的参数名完全对应,用解包写法非常简洁 类似的, *args是位置参数解包,**kwargs是关键字参数解包
5. 生成器表达式求最大值
我们在生成下一个ID的时候用到了:
return max(todo.id for todo in self.todos) + 1这里todo.id for todo in self.todos是一个生成器表达式,它和列表推导式很像,但是不会一次性把所有结果都生成到内存里,而是迭代一个生成一个,对于大列表来说更省内存,这里用max()直接迭代就可以得到结果,非常优雅。
6. JSON数据存储详解
我们用JSON格式把数据保存到文件todos.json中,JSON是轻量级的数据交换格式,Python内置了json模块:
json.dump(data, f):把Python数据对象写入JSON文件json.load(f):从JSON文件读取数据,解析成Python对象ensure_ascii=False:默认JSON会把所有非ASCII字符转成\uXXXX编码,这个参数会让中文直接保存为UTF-8中文,方便查看indent=2:按2个空格缩进格式化输出,让JSON文件更容易阅读,如果不写,会输出成一整行
为什么我们不直接保存Todo对象?因为JSON只能序列化基础数据类型(字典、列表、字符串、数字、布尔),不能直接序列化自定义类的对象,所以我们需要手动把每个Todo对象转成字典再保存。
7. 现代路径处理:pathlib
我们用pathlib.Path来处理文件路径:
path = Path(self.filename)if not path.exists():self.todos = []return相比于传统的os.path.exists(self.filename),pathlib是面向对象的写法,更简洁,更易读,而且跨平台兼容性更好,自动处理Windows和macOS/Linux不同的路径分隔符,是现在Python官方推荐的路径处理方式,从Python 3.4开始就内置支持了。
8. with语句上下文管理器
我们所有文件操作都用了with语句:
with open(self.filename, "r", encoding="utf-8") as f: data = json.load(f)with是Python的上下文管理器,它会自动帮我们管理文件资源:
进入 with块的时候自动打开文件退出 with块的时候自动关闭文件,不管代码是正常结束还是抛出异常,都会保证文件正确关闭不用我们手动写 f.close(),不会忘记关闭文件导致资源泄漏,非常安全推荐
9. 无限主循环与菜单分支
程序主入口用了while True无限循环:
while True:# 显示菜单# 处理用户选择# 直到用户选择5退出才break这是命令行交互程序最常用的结构:程序一直运行等待用户输入,直到用户主动选择退出。我们用if...elif...else对应用户不同的菜单选项,结构清晰。
10. 异常处理让程序更健壮
用户输入ID的时候,我们加了异常处理:
try: todo_id = int(input("请输入要标记的待办ID: "))self.mark_done(todo_id)except ValueError:print("❌ 请输入合法的数字ID")如果用户不小心输入了不是数字的内容(比如abc),int()转换会抛出ValueError异常,如果我们不捕获,程序会直接崩溃退出。加上异常处理之后,程序只会提示用户输入错误,不会崩溃,提升了用户体验,这就是健壮性。
11. 列表的enumerate和pop操作
删除待办的时候我们用到:
for i, todo in enumerate(self.todos):if todo.id == todo_id: deleted = self.todos.pop(i)enumerate()函数同时拿到列表的索引i和元素todo,这样我们找到要删除的元素之后,可以直接用pop(i)把对应索引的元素删除pop()方法会把删除的元素返回给我们,所以我们可以打印出来告诉用户删除了哪个待办因为我们的待办ID是全局递增的,不会重复,所以找到第一个匹配的就可以直接返回了
12. if __name__ == "__main__":的作用
最后我们用了这个经典写法:
if __name__ == "__main__": manager = TodoManager() manager.run()很多新手不理解这行到底干嘛的,记住两个作用:
当我们直接运行这个文件的时候(比如 python todo.py),__name__变量会被自动设置为"__main__",所以这段代码会执行,启动程序当我们把这个文件作为模块导入的时候(比如 import todo),__name__会被设置为模块名"todo",所以这段代码不会执行,不会自动启动程序,方便我们在别的代码里导入这里面的类复用
这是Python里非常重要的一个习惯,几乎所有可执行的Python文件都会加上这行判断。
🎮 运行演示
我们来看一下运行效果:
🎉 欢迎使用个人待办事项管理器!===== 菜单 =====1. 查看所有待办2. 添加新待办3. 标记已完成4. 删除待办5. 退出程序请输入你的选择(1-5): 2请输入待办内容: 学习Python✅ 添加待办成功:学习Python===== 菜单 =====...请输入你的选择(1-5): 2请输入待办内容: 写作业✅ 添加待办成功:写作业===== 菜单 =====...请输入你的选择(1-5): 1📝 你的待办事项:1. 🔲 学习Python2. 🔲 写作业请输入你的选择(1-5): 3请输入要标记的待办ID: 1🎉 标记已完成:学习Python请输入你的选择(1-5): 1📝 你的待办事项:1. ✅ 学习Python2. 🔲 写作业完美!所有功能都正常,退出程序再打开,数据还是存在的,因为我们保存到文件里了。
生成的todos.json文件内容大概是这样:
[{"id": 1,"content": "学习Python","done": true},{"id": 2,"content": "写作业","done": false}]清晰易懂,方便查看。
✨ 你可以扩展的功能(附实现思路)
这个基础版本已经可以正常使用了,你可以自己动手扩展更多功能,练习学到的知识点,这里给你一些思路:
按状态筛选待办:只显示未完成或者只显示已完成
实现思路:在菜单增加一个选项“6. 查看未完成待办”,遍历 self.todos的时候只打印todo.done == False的待办就可以了知识点:条件过滤、循环遍历 编辑已存在的待办内容
实现思路:增加一个菜单选项,让用户输入要编辑的ID,然后输入新内容,找到对应待办之后修改 todo.content = 新内容,然后调用save_to_file()保存就好了知识点:对象属性修改、文件保存 给待办添加截止日期
实现思路:在 Todo类的构造函数里增加一个deadline参数,添加待办的时候让用户输入日期,然后保存的时候把日期也存进JSON里。显示的时候把截止日期也打印出来,还可以判断如果超过截止日期还没完成,可以标红提醒知识点:类结构扩展、日期处理,可以用Python内置的 datetime模块处理日期比较分类管理待办:分成工作/生活/学习等不同分类
实现思路:给 Todo增加一个category属性,添加的时候让用户选择分类,增加按分类筛选查看的功能知识点:属性扩展、过滤功能,可以让用户输入分类名,遍历只显示对应分类的待办 统计待办完成率
实现思路:统计 len(self.todos)是总数量,统计sum(1 for todo in self.todos if todo.done)是已完成数量,然后计算完成率打印出来,还可以告诉你未完成有多少个知识点:生成器表达式、统计计算 搜索待办:按关键词搜索待办内容
实现思路:让用户输入关键词,遍历所有待办,判断 关键词 in todo.content,把包含关键词的结果打印出来知识点:字符串成员运算、过滤
动手改造一下,把它变成你自己专属的待办管理器吧!每改一点就运行试一下,慢慢你就会越来越熟练~
🎉 恭喜你完成了零基础教程!
回顾一下,我们14天学了这些内容:
Day1:环境搭建,第一个Python程序 Day2:变量和基本数据类型 Day3:条件判断if语句 Day4:循环for和while Day5:列表和字典 Day6:函数定义和使用 Day7:字符串常用操作 Day8:异常处理try...except Day9:模块和包的使用 Day10:常用标准模块介绍 Day11:面向对象入门:类和对象 Day12:面向对象进阶:魔法方法、属性 Day13:文件操作与IO Day14:综合实战项目
从完全零基础,到能自己写一个完整可运行的实用项目,你已经跨出了最关键的一步!
编程是练出来的,不是看出来的,一定要多写代码,多做项目,遇到问题多搜索多思考,你很快就能成为Python高手!
最后,如果这个教程对你有帮助,欢迎分享给更多想要学习Python的朋友~
Happy Coding! 👨💻👩💻
夜雨聆风