简介:本资源是一个基于Python开发的轻量级桌面通讯录系统实现方案,面向计算机、软件工程及通信工程等专业的本科生课程设计与毕业设计实践需求,解决联系人信息管理、增删改查、数据持久化与基础UI交互等典型应用场景。压缩包共10个文件(70KB),包含4个核心Python源码(含主界面TableUi.py、信息弹窗InfoDlg.py及数据库操作模块)、2个Qt Designer设计的.ui界面文件、1个SQLite数据库(peopleinfo.db)、1个图标资源(ico/png)、1个qrc资源编译文件及1个预编译的资源模块,完整覆盖GUI构建、数据存储与业务逻辑三层结构。已有221人学习下载,提供可直接运行的完整项目骨架,含清晰目录组织、本地数据库集成、表单验证逻辑与基础CRUD功能实现,便于学生快速理解Python+PyQt5桌面应用开发全流程,并在此基础上拓展搜索、分组、导入导出等进阶功能。
1. 项目概述与核心价值
最近在整理旧项目时,翻出来一个几年前写的通讯录系统,用Python实现的,打包成了一个zip文件。这个项目虽然看起来简单,就是个“增删改查”的典型应用,但我觉得它特别适合用来理解Python如何从零开始构建一个完整的、带数据持久化功能的小型桌面应用。无论是刚学完Python基础语法,想找个练手项目巩固一下的新手,还是已经工作、想快速重温一下文件操作和面向对象设计的老手,这个项目都能提供一条清晰的实践路径。它不依赖任何复杂的数据库(比如MySQL或MongoDB),而是直接使用Python内置的json或pickle模块操作文件来存储数据,这让整个项目的依赖极简,环境配置几乎零成本。通过这个项目,你能把散落的知识点——类与对象、列表字典操作、文件读写、异常处理、用户交互逻辑——像串珠子一样串联起来,形成一个可运行、有实际功能的作品。接下来,我就把这个“通讯录系统.zip”拆开,从设计思路到每一行关键代码,再到我踩过的坑和优化技巧,毫无保留地分享给你。
2. 系统整体设计与架构解析
2.1 为什么选择纯Python与文件存储?
很多教程一上来就教用数据库,但对于一个通讯录这样的轻量级应用,杀鸡焉用牛刀。我选择纯Python+文件存储的方案,主要基于以下几点考量:
- 极简部署与零依赖:用户只需要有Python环境(3.6以上版本均可),解压即用。不需要安装额外的数据库服务(如MySQL Server)或配置连接,避免了环境差异带来的各种“玄学”问题。这对于教学演示、个人工具或作为大项目中的一个独立模块来说,非常友好。
- 聚焦核心逻辑:剥离了数据库连接、SQL语句编写等外围知识,我们可以把全部精力集中在业务逻辑本身:如何设计联系人数据结构、如何组织代码模块、如何实现用户交互。这有助于初学者建立清晰的编程思维,而不是被复杂的配置劝退。
- 理解数据持久化的本质:无论是用
json还是pickle,本质都是将内存中的数据结构(列表、字典)序列化成字节流,写入硬盘文件。这个过程让你直观地理解什么是“持久化”,以及程序关闭后数据如何得以保存。这是理解更高级数据存储技术(如数据库)的基石。
当然,这个方案也有其局限性,比如不适合海量数据(性能问题)和高并发访问(文件锁问题)。但对于一个学习型项目或个人使用的通讯录,这些都不是问题。
2.2 核心模块与功能规划
一个完整的通讯录系统,至少需要包含以下几个核心功能模块,我称之为“CRUD+L”,即创建、读取、更新、删除和列表展示:
- 联系人模型 (Contact Model):定义一个
Contact类,用来表示一个联系人的所有信息。通常包括姓名、手机号,还可以扩展邮箱、地址、分组等字段。这个类是系统的数据基石。 - 通讯录管理核心 (AddressBook Core):定义一个
AddressBook类,它内部维护一个联系人列表(通常是Contact对象的列表),并对外提供所有操作方法,如添加联系人、删除联系人、查找联系人、修改联系人信息、显示所有联系人等。这个类是系统的业务逻辑核心。 - 数据持久化层 (Persistence Layer):负责将
AddressBook管理的数据保存到文件,以及从文件加载数据到内存。这里会用到json模块(人类可读,易调试)或pickle模块(支持Python所有对象,但二进制格式)。 - 用户交互界面 (User Interface):提供一个命令行菜单,让用户可以通过输入数字或字母来选择要执行的操作。这是系统与用户直接打交道的部分。
整个系统的数据流非常清晰:用户通过UI发出指令 ->AddressBook类执行业务逻辑(操作内存中的联系人列表)-> 数据持久化层在适当的时候(如退出时)将数据保存到文件,或启动时从文件加载数据。
2.3 技术选型:json vs pickle
在数据持久化方案上,json和pickle是最常见的两个选择。我的项目里最终选择了json,原因如下:
| 特性 | json模块 | pickle模块 | 本项目选择理由 |
|---|---|---|---|
| 可读性 | 优秀。保存为纯文本,可直接用记事本打开查看、编辑。 | 差。保存为二进制格式,人类无法直接阅读。 | 通讯录数据量小,可读性利于调试和手动应急修改。 |
| 安全性 | 相对安全。只能序列化基本数据类型(dict, list, str, int, float, bool, None)。 | 有风险。可以序列化几乎任何Python对象,反序列化时可能执行任意代码。 | 对于来源不明的数据文件,pickle存在安全漏洞。我们的数据是自生成的,风险低,但json更规范。 |
| 跨语言 | 支持。JSON是通用数据交换格式,几乎所有编程语言都支持。 | 仅限Python。其他语言无法直接读取.pkl文件。 | 虽然本项目不涉及跨语言,但使用通用格式是更好的实践。 |
| 序列化范围 | 有限。无法直接序列化自定义类的实例对象。 | 广泛。可以直接序列化自定义类的实例对象。 | 我们需要将Contact对象列表保存,json无法直接处理。这是关键难点,也是需要额外处理的地方。 |
注意:
json不能直接序列化Contact对象,但我们可以通过将对象转换为字典(__dict__属性或自定义to_dict()方法)来解决。反序列化时,再从字典还原为对象。这个过程虽然多了一步,但使得数据格式清晰、可控,是更推荐的做法。
3. 核心代码实现与逐行解析
3.1 联系人模型 (Contact Class) 的实现
这是整个系统的数据单元。我们不仅要定义数据字段,还要考虑如何打印它、如何比较两个联系人是否相同(例如,根据手机号判断),以及如何将其转换为可被json序列化的字典。
# contact.py class Contact: """联系人类,定义单个联系人的属性和行为""" def __init__(self, name, phone, email='', address='', group='默认'): """ 初始化联系人 :param name: 姓名 (必填) :param phone: 手机号 (必填) :param email: 电子邮箱 (可选) :param address: 地址 (可选) :param group: 分组 (可选,默认为‘默认’) """ self.name = name self.phone = phone self.email = email self.address = address self.group = group def __str__(self): """定义打印联系人时的格式,方便阅读""" return f"姓名: {self.name}, 电话: {self.phone}, 邮箱: {self.email}, 地址: {self.address}, 分组: {self.group}" def __repr__(self): """定义在解释器中直接显示对象时的格式,通常用于调试""" return f"Contact(name='{self.name}', phone='{self.phone}')" def to_dict(self): """将Contact对象转换为字典,用于json序列化""" return { 'name': self.name, 'phone': self.phone, 'email': self.email, 'address': self.address, 'group': self.group } @classmethod def from_dict(cls, data): """从字典创建Contact对象,用于json反序列化""" # 这里使用字典的get方法,可以避免因字典缺少某个键而报错 return cls( name=data.get('name', ''), phone=data.get('phone', ''), email=data.get('email', ''), address=data.get('address', ''), group=data.get('group', '默认') ) # 可选:实现__eq__方法,方便根据手机号判断两个联系人是否相同 def __eq__(self, other): if isinstance(other, Contact): return self.phone == other.phone return False关键点解析与避坑指南:
__init__方法中的默认参数:email、address、group都设置了默认值。这样在创建联系人时,如果只提供name和phone,其他字段会自动填充为默认值,使API更友好。__str__与__repr__的区别:__str__是给用户看的(如print(contact)),格式友好;__repr__是给开发者看的(如在交互式环境中直接输入变量名),应包含足够的信息以重新创建该对象。两者都实现是好习惯。to_dict和from_dict类方法:这是解决json无法序列化自定义对象的关键。to_dict将对象属性打包成字典;from_dict是一个类方法(用@classmethod装饰),它不依赖于某个实例,而是像工厂一样,根据传入的字典数据创建一个新的Contact对象。这种设计模式非常清晰。- 使用
data.get(key, default):在from_dict中,我们使用get方法从字典取值。这比直接用data['key']更安全,因为如果旧的保存文件格式中缺少某个键(比如后来新增了group字段),程序不会崩溃,而是使用默认值。
3.2 通讯录管理核心 (AddressBook Class) 的实现
这个类是业务逻辑的集大成者,它内部管理着一个联系人列表,并提供各种操作方法。
# address_book.py import json from contact import Contact class AddressBook: """通讯录管理类,核心业务逻辑""" def __init__(self, filename='contacts.json'): """ 初始化通讯录 :param filename: 数据存储的文件名 """ self.filename = filename self.contacts = [] # 用于在内存中存储所有Contact对象的列表 self.load_from_file() # 初始化时自动从文件加载数据 def add_contact(self, contact): """添加一个新联系人""" # 添加前可以做一些校验,比如手机号是否已存在 if contact in self.contacts: print(f"警告:手机号 {contact.phone} 已存在,添加失败。") return False self.contacts.append(contact) print(f"联系人 {contact.name} 添加成功。") self.save_to_file() # 每次修改后自动保存(也可改为手动触发) return True def delete_contact(self, phone): """根据手机号删除联系人""" for i, contact in enumerate(self.contacts): if contact.phone == phone: deleted_contact = self.contacts.pop(i) print(f"联系人 {deleted_contact.name} 删除成功。") self.save_to_file() return True print(f"未找到手机号为 {phone} 的联系人。") return False def find_contact(self, keyword): """根据姓名或手机号查找联系人(支持模糊搜索)""" results = [] keyword_lower = keyword.lower() for contact in self.contacts: # 搜索姓名或手机号中包含关键词的联系人(不区分大小写) if (keyword_lower in contact.name.lower()) or (keyword in contact.phone): results.append(contact) return results def update_contact(self, phone, **kwargs): """根据手机号更新联系人信息,kwargs为要更新的字段字典""" for contact in self.contacts: if contact.phone == phone: # 遍历传入的更新字段 for key, value in kwargs.items(): if hasattr(contact, key): # 检查联系人是否有该属性 setattr(contact, key, value) # 动态设置属性值 else: print(f"警告:联系人没有属性 '{key}',已忽略。") print(f"联系人 {contact.name} 信息更新成功。") self.save_to_file() return True print(f"未找到手机号为 {phone} 的联系人,更新失败。") return False def display_all(self, group_filter=None): """显示所有联系人,可按分组过滤""" filtered_contacts = self.contacts if group_filter: filtered_contacts = [c for c in self.contacts if c.group == group_filter] if not filtered_contacts: print("通讯录为空或没有符合条件的联系人。") return print(f"\n=== 通讯录列表 ({'所有分组' if not group_filter else '分组: '+group_filter}) ===") for idx, contact in enumerate(filtered_contacts, 1): print(f"{idx}. {contact}") print("=" * 50) def save_to_file(self): """将通讯录保存到JSON文件""" try: # 将所有Contact对象转换为字典列表 data = [contact.to_dict() for contact in self.contacts] with open(self.filename, 'w', encoding='utf-8') as f: json.dump(data, f, ensure_ascii=False, indent=4) # indent使JSON文件格式化,便于阅读 # print("数据已保存。") # 可注释掉,避免每次保存都输出 except IOError as e: print(f"保存文件时出错: {e}") def load_from_file(self): """从JSON文件加载通讯录""" try: with open(self.filename, 'r', encoding='utf-8') as f: data = json.load(f) # 将字典列表转换回Contact对象列表 self.contacts = [Contact.from_dict(item) for item in data] print(f"已从 {self.filename} 加载 {len(self.contacts)} 个联系人。") except FileNotFoundError: # 如果文件不存在,则创建一个空列表,这是第一次运行 print("未找到数据文件,将创建新的通讯录。") self.contacts = [] except json.JSONDecodeError: # 如果文件内容不是有效的JSON,可能是文件损坏 print("警告:数据文件格式错误,将初始化空通讯录。") self.contacts = []关键点解析与避坑指南:
- 自动加载与保存:在
__init__中调用load_from_file(),使得AddressBook对象一旦创建,就自动载入历史数据。在add_contact,delete_contact,update_contact等方法中调用save_to_file(),实现了数据的实时持久化。你也可以设计一个“手动保存”模式,但自动保存对用户更友好。 - 查找与删除的逻辑:
delete_contact和update_contact都依赖于遍历查找手机号。这里使用enumerate来同时获取索引和元素,找到后通过索引pop删除效率更高(O(n)查找+O(1)删除)。如果联系人非常多,可以考虑用字典以手机号为键来存储,实现O(1)的查找和删除。 - 模糊搜索的实现:
find_contact方法将关键词和姓名都转换为小写进行匹配,实现了不区分大小写的模糊搜索。这是一个提升用户体验的小细节。 - 灵活的更新方法:
update_contact使用了**kwargs(关键字参数)来接收要更新的字段。hasattr和setattr这两个内置函数允许我们动态地检查和设置对象的属性,使得这个方法非常通用,无需为每个字段单独写更新逻辑。 - 健壮的文件操作:
save_to_file中ensure_ascii=False保证了中文等非ASCII字符能正确保存,indent=4让生成的JSON文件有缩进,易于阅读。load_from_file中处理了FileNotFoundError(首次运行)和json.JSONDecodeError(文件损坏)两种异常。这是非常重要的错误处理!如果没有这些处理,程序遇到损坏的文件就会直接崩溃,用户体验极差。我们的处理方式是降级:初始化一个空列表,让程序可以继续运行。
3.3 用户交互界面 (CLI Menu) 的实现
这是面向用户的终端界面,提供一个循环菜单,根据用户输入调用AddressBook的相应方法。
# main.py from address_book import AddressBook from contact import Contact def display_menu(): """显示主菜单""" print("\n" + "="*30) print(" 通讯录管理系统") print("="*30) print(" 1. 添加联系人") print(" 2. 删除联系人") print(" 3. 查找联系人") print(" 4. 修改联系人") print(" 5. 显示所有联系人") print(" 6. 按分组显示") print(" 7. 退出系统") print("="*30) def get_contact_info_from_input(): """从用户输入收集联系人信息""" name = input("请输入姓名: ").strip() while not name: # 简单的非空校验 name = input("姓名不能为空,请重新输入: ").strip() phone = input("请输入手机号: ").strip() # 这里可以添加更复杂的手机号格式校验(正则表达式) while not phone: phone = input("手机号不能为空,请重新输入: ").strip() email = input("请输入邮箱 (可选,直接回车跳过): ").strip() address = input("请输入地址 (可选,直接回车跳过): ").strip() group = input("请输入分组 (可选,默认为‘默认’): ").strip() if not group: group = '默认' return name, phone, email, address, group def main(): """主程序入口""" book = AddressBook() # 初始化通讯录,会自动加载数据 while True: display_menu() choice = input("请选择操作 (1-7): ").strip() if choice == '1': # 添加联系人 name, phone, email, address, group = get_contact_info_from_input() new_contact = Contact(name, phone, email, address, group) book.add_contact(new_contact) elif choice == '2': # 删除联系人 phone = input("请输入要删除联系人的手机号: ").strip() if phone: # 确认输入非空 confirm = input(f"确定要删除手机号为 {phone} 的联系人吗?(y/n): ").strip().lower() if confirm == 'y': book.delete_contact(phone) else: print("输入不能为空。") elif choice == '3': # 查找联系人 keyword = input("请输入要查找的姓名或手机号(支持模糊搜索): ").strip() if keyword: results = book.find_contact(keyword) if results: print(f"\n找到 {len(results)} 个结果:") for contact in results: print(f" - {contact}") else: print("未找到匹配的联系人。") else: print("搜索关键词不能为空。") elif choice == '4': # 修改联系人 phone = input("请输入要修改联系人的手机号: ").strip() if not phone: print("手机号不能为空。") continue # 先查找是否存在 results = book.find_contact(phone) if not results: print(f"未找到手机号为 {phone} 的联系人。") continue print(f"\n当前信息: {results[0]}") print("\n请输入要修改的字段和新值(留空表示不修改):") updates = {} name = input(f"姓名 [{results[0].name}]: ").strip() if name: updates['name'] = name # 注意:手机号作为唯一标识,通常不允许修改。若要修改,需先删除旧记录再添加新记录。 # 这里我们选择不允许修改手机号。 # phone_new = input(f"手机号 [{results[0].phone}]: ").strip() # if phone_new: # updates['phone'] = phone_new email = input(f"邮箱 [{results[0].email}]: ").strip() if email: updates['email'] = email address = input(f"地址 [{results[0].address}]: ").strip() if address: updates['address'] = address group = input(f"分组 [{results[0].group}]: ").strip() if group: updates['group'] = group if updates: book.update_contact(phone, **updates) else: print("未作任何修改。") elif choice == '5': # 显示所有联系人 book.display_all() elif choice == '6': # 按分组显示 group_name = input("请输入要筛选的分组名 (直接回车显示所有): ").strip() book.display_all(group_filter=group_name if group_name else None) elif choice == '7': # 退出系统 print("感谢使用通讯录管理系统,再见!") # AddressBook采用自动保存,此处无需手动保存 break else: print("无效的选择,请输入 1 到 7 之间的数字。") input("\n按回车键继续...") # 暂停,让用户看清结果 if __name__ == "__main__": main()关键点解析与避坑指南:
- 输入校验与用户体验:在
get_contact_info_from_input中,对姓名和手机号做了简单的非空校验。在实际项目中,应该用正则表达式对手机号、邮箱格式做更严格的校验。对于修改操作,提供了当前值作为提示([当前值]),留空则保持原样,这是非常人性化的设计。 - 关键操作确认:对于删除这种危险操作,增加了二次确认(
y/n)环节,防止误操作。 - 手机号作为唯一标识:在修改功能中,我刻意禁止了修改手机号。因为在这个简单设计中,手机号是查找和识别联系人的关键字段。如果允许修改,逻辑会变得复杂(需要先按旧手机号找到记录,删除,再用新手机号创建一条新记录)。在更复杂的设计中,可以为每个联系人分配一个唯一的ID(如UUID),这样手机号就可以像其他字段一样随意修改了。
if __name__ == "__main__":的作用:这是一个Python的惯用法。它保证了当你直接运行main.py时,main()函数会被执行;而当main.py被其他文件作为模块导入时,main()不会自动执行。这对于代码的复用和测试非常重要。- 清晰的交互流程:每个操作后使用
input("\n按回车键继续...")暂停,让用户有时间阅读输出信息,然后清屏(通过菜单重绘)继续,避免了信息一闪而过的问题。
4. 项目运行、打包与扩展思路
4.1 如何运行这个项目?
- 环境准备:确保你的电脑安装了Python(3.6或以上版本)。可以在命令行输入
python --version或python3 --version检查。 - 解压与运行:将“通讯录系统.zip”解压到一个文件夹。你会看到
contact.py,address_book.py,main.py三个核心文件。在命令行中进入该文件夹,直接运行python main.py即可启动系统。 - 首次运行:首次运行会提示“未找到数据文件,将创建新的通讯录。”,并在当前目录生成一个空的
contacts.json文件。之后的所有操作数据都会自动保存到这个文件中。
4.2 从源码到可执行文件:使用PyInstaller打包
如果你想把这个Python脚本分享给没有安装Python的朋友,可以将其打包成一个独立的.exe(Windows)或可执行文件(macOS/Linux)。这里推荐使用PyInstaller。
- 安装PyInstaller:在命令行中运行
pip install pyinstaller。 - 执行打包:在项目根目录(
main.py所在目录)下,运行:pyinstaller -F -w -i icon.ico main.py-F: 打包成单个可执行文件。-w: 运行时不显示命令行窗口(对于GUI程序常用,我们这是控制台程序,可加可不加。如果加了,打印信息会看不到,调试时建议去掉)。-i icon.ico: 为可执行文件指定一个图标(需要准备一个.ico格式的图标文件)。- 最简单的打包命令是
pyinstaller -F main.py。
- 获取成品:打包完成后,在项目目录下会生成一个
dist文件夹,里面的main.exe(Windows)就是打包好的程序。你可以把这个.exe文件和contacts.json数据文件一起发给别人使用。
注意:PyInstaller打包时,有时会因为动态导入或路径问题导致打包后的程序找不到模块。如果遇到问题,可以尝试在
main.py开头显式导入所有依赖模块(虽然我们已经做了),或者使用--hidden-import参数。对于我们的简单项目,通常直接打包就能成功。
4.3 功能扩展与优化方向
这个基础版本已经可用,但还有很多可以打磨和扩展的地方,这也是你深入学习的好机会:
- 数据校验强化:
- 使用
re模块(正则表达式)对手机号、邮箱进行格式校验。 - 添加姓名长度限制、去空格处理等。
- 使用
- 搜索功能增强:
- 支持多条件组合搜索(如同时按姓名和分组查找)。
- 实现更高效的搜索,例如预先建立姓名的拼音索引,支持拼音首字母搜索。
- 数据备份与恢复:
- 增加“导出通讯录”功能,可以将数据另存为CSV或Excel格式,方便用其他软件打开。
- 增加“导入通讯录”功能,从CSV文件批量导入联系人。
- 图形化界面 (GUI):
- 使用
tkinter(Python标准库)、PyQt5或Kivy等库为通讯录开发一个图形窗口界面,用户体验会大幅提升。
- 使用
- 网络化与多端同步:
- 将数据存储从本地文件改为网络数据库(如SQLite配合Web API,或直接使用云数据库服务)。
- 开发一个简单的Flask或FastAPI后端,配合前端(网页或手机App)实现多端数据同步。这就能从一个本地工具升级为一个真正的“系统”。
5. 常见问题与排查技巧实录
在实际编写和运行这个项目的过程中,你可能会遇到以下问题。这里我把自己踩过的坑和解决方法记录下来:
Q1: 运行程序后,添加了联系人,但关闭程序再打开,数据没了?
- 排查:首先检查程序所在目录下是否生成了
contacts.json文件。如果没有,说明保存失败。 - 可能原因与解决:
- 文件写入权限问题:确保程序对当前目录有写权限。可以尝试以管理员身份运行,或换一个目录。
- 代码中的保存逻辑未执行:检查
add_contact、delete_contact等方法中是否调用了self.save_to_file()。确保在__init__中正确调用了load_from_file()。 - 异常导致保存中断:在
save_to_file方法中,如果json.dump之前发生错误(如对象无法序列化),可能导致保存失败。确保所有要保存的对象都有to_dict方法。可以在save_to_file里加更详细的try...except打印错误信息。
Q2: 错误提示ModuleNotFoundError: No module named 'contact'
- 排查:这通常是因为Python找不到你写的模块。
- 解决:
- 确保
contact.py、address_book.py、main.py三个文件在同一个目录下。 - 确保你是在包含这三个文件的目录下运行的
python main.py。 - 如果文件结构更复杂(比如有子文件夹),需要在子文件夹中创建
__init__.py文件(可以是空的),并使用相对导入(如from .contact import Contact)。
- 确保
Q3: JSON文件里中文显示为Unicode码(如\u4e2d\u6587)
- 原因:
json.dump()时没有设置ensure_ascii=False。 - 解决:在
address_book.py的save_to_file方法中,确保json.dump(data, f, ensure_ascii=False, indent=4)。
Q4: 想修改手机号,但程序里不允许,怎么改?
- 临时方案:直接打开
contacts.json文件,找到对应的联系人记录,手动修改phone字段的值。这就是使用JSON格式存储的另一个好处——可手动编辑。 - 永久方案:修改
update_contact方法的逻辑。这需要分两步:1) 用旧手机号找到联系人,获取其全部信息;2) 用新手机号创建一个新的Contact对象(其他信息不变),删除旧对象,添加新对象。注意要处理好手机号唯一性的校验。
Q5: 联系人很多时,查找和删除感觉有点慢。
- 原因:当前实现是线性查找(遍历列表),时间复杂度是O(n)。当数据量上千时,性能瓶颈会显现。
- 优化思路:使用字典(
dict)作为主要存储结构,以手机号(或一个自增ID)作为键,Contact对象作为值。这样查找和删除的时间复杂度可以降到O(1)。代价是显示所有联系人时(需要遍历字典的值)和保持插入顺序(Python 3.7+的dict已保持插入顺序)可能略有不同,但通常利大于弊。你可以尝试重构AddressBook类,将self.contacts = []改为self.contacts = {},并相应修改所有方法。这是一个非常好的进阶练习。
本文还有配套的精品资源,点击获取