第十八节:项目实战2:简易通讯录(面向对象+文件持久化实现)

上一篇咱们通过猜数字游戏,完整体验了函数级模块化的开发流程,巩固了循环、条件判断、异常处理等基础技能,也建立了"需求分析→架构设计→实现优化"的项目思维。但那款游戏存在明显局限:数据仅在内存中临时存储,程序关闭后所有记录全部丢失,且未采用面向对象编程思想,无法应对多实体、复杂业务逻辑的场景。今天,咱们就聚焦思维导图"项目实战2"模块,开发一款更贴近实际应用的简易通讯录,融合面向对象编程、JSON文件持久化、模块化架构、全场景异常处理等全套技能,解决数据持久化核心问题,实现联系人的增删改查全功能。

本次通讯录实战将严格对齐思维导图框架,从需求分析、架构设计、分层开发到测试优化,层层递进、细节拉满。不仅要实现基础功能,更要吃透面向对象的封装思想与文件持久化的核心逻辑,让你突破"函数级开发"的局限,掌握中小型业务项目的综合开发能力,同时为下一章"项目实战3:合规爬虫入门"------一款融合网络请求、数据解析与存储的实战项目,做好技术与思路的双重铺垫。

一、项目需求分析:明确通讯录核心目标

通讯录作为高频实用工具,核心需求是"安全存储联系人信息、便捷管理数据"。结合思维导图"需求分析"模块的拆解维度,咱们从核心功能、用户体验、技术栈选型三个层面,明确具体要求,为后续开发划定清晰边界。

1. 核心功能(必实现)

  • 新增联系人:录入姓名、电话、邮箱(可选)信息,自动避免重复姓名(姓名作为唯一标识);

  • 查询联系人:支持按姓名精准查询,展示联系人完整信息;支持展示所有联系人,适配空列表场景;

  • 修改联系人:按姓名定位联系人,修改其电话、邮箱信息,支持单独修改一项或多项;

  • 删除联系人:按姓名精准删除联系人,支持删除前二次确认,避免误操作;

  • 数据持久化:将联系人数据存储在JSON文件中,程序启动时加载数据,操作后自动保存,确保程序关闭后数据不丢失。

2. 细节优化(提升体验与稳定性)

  • 输入合法性校验:电话需为11位数字(手机号标准),邮箱需含@符号,姓名非空,非法输入给出友好提示;

  • 异常场景处理:查询/修改/删除不存在的联系人、JSON文件损坏、权限不足等场景,均需捕获异常并提示,避免程序崩溃;

  • 交互体验优化:提供清晰的功能菜单,操作后给出明确反馈(如"新增成功""联系人不存在"),删除前二次确认;

  • 数据容错:程序首次运行无JSON文件时,自动创建空文件;文件格式错误时,提示用户并初始化空数据。

3. 技术栈选型(贴合思维导图知识点)

本次项目整合前期核心知识点,不引入第三方模块,技术栈完全贴合思维导图"技术应用"模块要求,具体包括:

  • 面向对象编程:封装Contact实体类(存储联系人属性)、AddressBook业务类(封装增删改查逻辑),体现封装、单一职责原则;

  • 文件持久化:使用JSON模块实现数据读写,解决数据临时存储问题,适配文件不存在、格式错误等场景;

  • 异常处理:捕获JSONDecodeError、FileNotFoundError、PermissionError等异常,覆盖文件操作、输入校验全场景;

  • 模块化开发:按"实体类→工具类→主程序"拆分代码,结构清晰、可维护;

  • 基础语法:循环控制菜单交互,条件判断处理业务逻辑,字符串方法实现输入校验。

二、项目架构设计:面向对象+模块化拆分

结合思维导图"架构设计"模块的核心思路,本次通讯录采用"三层模块化+面向对象"架构,将代码按"实体层、业务工具层、主程序层"拆分,实现"高内聚、低耦合",避免逻辑堆砌。项目结构如下(4个文件,分工明确,可扩展):

python 复制代码
simple_address_book/  # 通讯录项目根目录
├── contact.py        # 实体层:封装Contact联系人实体类
├── address_book.py   # 业务工具层:封装AddressBook业务逻辑与持久化
├── utils.py          # 通用工具层:封装输入校验、异常提示等通用功能
└── main.py           # 主程序层:菜单交互、启动程序入口

模块职责划分(对齐思维导图)

  • contact.py(实体层):仅封装联系人属性(姓名、电话、邮箱),提供初始化方法与属性访问方式,不包含业务逻辑,确保实体纯度;

  • address_book.py(业务工具层):核心业务模块,封装AddressBook类,整合"增删改查+JSON读写"逻辑,调用Contact类创建实体,对外提供统一业务接口;

  • utils.py(通用工具层):封装通用工具函数(电话/邮箱校验、输入获取),可复用至其他项目,减少代码冗余;

  • main.py(主程序层):程序入口,提供菜单交互界面,调用AddressBook类的业务接口,不涉及具体业务逻辑与持久化实现。

设计核心:面向对象的"实体与业务分离"------Contact类只管"数据存储",AddressBook类只管"业务处理",主程序只管"交互",符合思维导图"单一职责"设计原则,后续迭代功能(如批量导入)无需改动核心逻辑。

三、分层开发实现:逐模块编写代码

咱们按"实体层→工具层→业务层→主程序层"的顺序编写代码,每一步都对齐思维导图"功能实现"模块要点,添加详细注释,确保代码可理解、可复现。

1. 实体层:contact.py(联系人实体类)

封装联系人属性,提供初始化方法,确保属性合法性(通过参数校验),体现面向对象封装思想。

python 复制代码
# contact.py:联系人实体类
class Contact:
    """联系人实体类:封装联系人属性,提供初始化与属性访问"""
    def __init__(self, name, phone, email=None):
        """
        初始化联系人
        参数:name-姓名(非空),phone-电话(11位数字),email-邮箱(可选)
        """
        self.name = name.strip()  # 姓名去空格,确保唯一性
        self.phone = phone.strip()
        self.email = email.strip() if email else None  # 邮箱可选,空则设为None

    def to_dict(self):
        """将联系人对象转为字典,用于JSON序列化(JSON无法直接存储对象)"""
        return {
            "name": self.name,
            "phone": self.phone,
            "email": self.email
        }

    @staticmethod
    def from_dict(data):
        """从字典构建联系人对象,用于JSON反序列化(读取文件后重建对象)"""
        return Contact(
            name=data["name"],
            phone=data["phone"],
            email=data.get("email")  # 兼容无邮箱的旧数据
        )

2. 通用工具层:utils.py(输入校验与工具函数)

封装通用校验逻辑,避免业务层代码冗余,适配输入校验全场景。

python 复制代码
# utils.py:通用工具函数
def is_valid_phone(phone):
    """校验手机号:必须为11位数字"""
    return phone.isdigit() and len(phone) == 11

def is_valid_email(email):
    """校验邮箱:非空时需包含@符号"""
    if not email:
        return True  # 邮箱可选,空则合法
    return "@" in email and "." in email.split("@")[-1]  # 简单校验,满足基础需求

def get_non_empty_input(prompt):
    """获取非空输入,避免用户输入空值"""
    while True:
        content = input(prompt).strip()
        if content:
            return content
        print("❌ 输入不能为空,请重新输入!")

def get_confirm(prompt):
    """获取用户确认(y/n),返回布尔值"""
    while True:
        choice = input(prompt).strip().lower()
        if choice in ["y", "n"]:
            return choice == "y"
        print("❌ 输入错误!请输入'y'确认或'n'取消。")

3. 业务工具层:address_book.py(核心业务与持久化)

封装通讯录核心业务逻辑与JSON持久化,调用Contact类与utils工具,对外提供统一业务接口,是项目核心模块。

python 复制代码
# address_book.py:通讯录业务类与持久化
import json
import os
from contact import Contact
from utils import is_valid_phone, is_valid_email

class AddressBook:
    """通讯录业务类:封装增删改查逻辑与JSON文件持久化"""
    def __init__(self, file_path="contacts.json"):
        self.file_path = file_path  # 数据存储文件路径
        self.contacts = []  # 存储Contact对象的列表
        self.load_data()  # 初始化时加载数据

    def load_data(self):
        """从JSON文件加载数据,初始化contacts列表"""
        try:
            # 检查文件是否存在,不存在则创建空文件
            if not os.path.exists(self.file_path):
                with open(self.file_path, 'w', encoding='utf-8') as f:
                    json.dump([], f)
                print(f"✅ 首次运行,已创建数据文件:{self.file_path}")
                return

            # 读取文件并反序列化
            with open(self.file_path, 'r', encoding='utf-8') as f:
                data_list = json.load(f)
            # 将字典列表转为Contact对象列表
            self.contacts = [Contact.from_dict(data) for data in data_list]
            print(f"✅ 数据加载成功,共读取到 {len(self.contacts)} 个联系人")
        except json.JSONDecodeError:
            print(f"⚠️  数据文件格式错误,已初始化空通讯录(请检查{self.file_path}文件)")
            self.contacts = []
        except PermissionError:
            print(f"❌ 权限不足,无法读取{self.file_path}文件,通讯录功能受限")
            self.contacts = []
        except Exception as e:
            print(f"⚠️  数据加载异常:{str(e)},已初始化空通讯录")
            self.contacts = []

    def save_data(self):
        """将contacts列表序列化,保存到JSON文件"""
        try:
            # 将Contact对象列表转为字典列表(JSON无法序列化对象)
            data_list = [contact.to_dict() for contact in self.contacts]
            with open(self.file_path, 'w', encoding='utf-8') as f:
                json.dump(data_list, f, ensure_ascii=False, indent=2)
            return True, "保存成功"
        except PermissionError:
            return False, "权限不足,无法保存数据"
        except Exception as e:
            return False, f"保存异常:{str(e)}"

    def add_contact(self, name, phone, email=None):
        """新增联系人:校验合法性,避免重复姓名"""
        # 校验输入合法性
        if not is_valid_phone(phone):
            return False, "手机号格式错误,需为11位数字"
        if not is_valid_email(email):
            return False, "邮箱格式错误,请包含@符号"
        # 校验姓名是否重复
        if any(contact.name == name for contact in self.contacts):
            return False, f"联系人「{name}」已存在,无法重复添加"
        # 创建联系人对象并添加
        new_contact = Contact(name, phone, email)
        self.contacts.append(new_contact)
        # 保存数据
        success, msg = self.save_data()
        if success:
            return True, f"联系人「{name}」新增成功"
        else:
            self.contacts.remove(new_contact)  # 保存失败,回滚操作
            return False, f"新增失败:{msg}"

    def find_contact(self, name):
        """按姓名查询联系人:返回Contact对象或None"""
        for contact in self.contacts:
            if contact.name == name:
                return contact
        return None

    def query_contact(self, name):
        """查询联系人详情:返回查询结果提示"""
        contact = self.find_contact(name)
        if not contact:
            return False, f"未找到联系人「{name}」"
        # 构造详情信息,适配无邮箱场景
        email_info = contact.email if contact.email else "未填写"
        detail = (f"📌 联系人详情\n"
                  f"姓名:{contact.name}\n"
                  f"电话:{contact.phone}\n"
                  f"邮箱:{email_info}")
        return True, detail

    def show_all_contacts(self):
        """展示所有联系人:返回联系人列表提示"""
        if not self.contacts:
            return False, "通讯录为空,无联系人可展示"
        # 构造所有联系人信息
        all_info = "📋 所有联系人列表\n" + "-"*30
        for idx, contact in enumerate(self.contacts, 1):
            email_info = contact.email if contact.email else "未填写"
            all_info += f"\n{idx}. 姓名:{contact.name} | 电话:{contact.phone} | 邮箱:{email_info}"
        return True, all_info

    def modify_contact(self, name, new_phone=None, new_email=None):
        """修改联系人:按姓名定位,支持单独修改电话/邮箱"""
        contact = self.find_contact(name)
        if not contact:
            return False, f"未找到联系人「{name}」,无法修改"
        # 校验并修改电话(非空则修改)
        if new_phone:
            if not is_valid_phone(new_phone):
                return False, "手机号格式错误,需为11位数字"
            contact.phone = new_phone
        # 校验并修改邮箱(非空则修改)
        if new_email is not None:  # 允许设为空字符串
            if not is_valid_email(new_email):
                return False, "邮箱格式错误,请包含@符号"
            contact.email = new_email.strip() or None
        # 保存数据
        success, msg = self.save_data()
        if success:
            return True, f"联系人「{name}」修改成功"
        else:
            return False, f"修改失败:{msg}"

    def delete_contact(self, name):
        """删除联系人:按姓名定位,支持二次确认"""
        contact = self.find_contact(name)
        if not contact:
            return False, f"未找到联系人「{name}」,无法删除"
        # 二次确认
        from utils import get_confirm
        if not get_confirm(f"⚠️  确定要删除联系人「{name}」吗?(y/n):"):
            return False, "已取消删除操作"
        # 删除并保存
        self.contacts.remove(contact)
        success, msg = self.save_data()
        if success:
            return True, f"联系人「{name}」删除成功"
        else:
            self.contacts.append(contact)  # 保存失败,回滚操作
            return False, f"删除失败:{msg}"

4. 主程序层:main.py(菜单交互入口)

提供清晰的菜单界面,接收用户操作指令,调用AddressBook类的业务接口,实现交互闭环。

python 复制代码
# main.py:通讯录主程序入口
from address_book import AddressBook
from utils import get_non_empty_input, get_confirm

def show_menu():
    """展示功能菜单"""
    print("\n" + "="*40)
    print("📱  简易通讯录管理系统  📱")
    print("="*40)
    print("1. 新增联系人")
    print("2. 查询联系人(按姓名)")
    print("3. 展示所有联系人")
    print("4. 修改联系人")
    print("5. 删除联系人")
    print("6. 退出系统")
    print("="*40)

def main():
    """主程序:菜单交互与业务调度"""
    # 初始化通讯录(自动加载数据)
    address_book = AddressBook()
    while True:
        show_menu()
        # 获取用户操作指令
        choice = input("请输入功能编号(1-6):").strip()
        if choice not in ["1", "2", "3", "4", "5", "6"]:
            print("❌ 输入错误!请输入1-6之间的功能编号。")
            continue

        # 1. 新增联系人
        if choice == "1":
            print("\n🔧 新增联系人")
            name = get_non_empty_input("请输入姓名:")
            phone = get_non_empty_input("请输入手机号(11位数字):")
            email = input("请输入邮箱(可选):").strip()
            success, msg = address_book.add_contact(name, phone, email)
            print(f"{'✅' if success else '❌'} {msg}")

        # 2. 查询联系人
        elif choice == "2":
            print("\n🔍 查询联系人")
            name = get_non_empty_input("请输入要查询的姓名:")
            success, msg = address_book.query_contact(name)
            print(f"{'✅' if success else '❌'} {msg}")

        # 3. 展示所有联系人
        elif choice == "3":
            print("\n📋 展示所有联系人")
            success, msg = address_book.show_all_contacts()
            print(msg)

        # 4. 修改联系人
        elif choice == "4":
            print("\n✏️  修改联系人")
            name = get_non_empty_input("请输入要修改的联系人姓名:")
            # 询问是否修改电话/邮箱
            new_phone = input("请输入新手机号(不修改请按回车):").strip() or None
            new_email = input("请输入新邮箱(不修改请按回车,清空请输入空格后回车):").strip()
            # 处理空邮箱(用户输入空格后回车,设为None)
            new_email = new_email if new_email != " " else None
            success, msg = address_book.modify_contact(name, new_phone, new_email)
            print(f"{'✅' if success else '❌'} {msg}")

        # 5. 删除联系人
        elif choice == "5":
            print("\n🗑️  删除联系人")
            name = get_non_empty_input("请输入要删除的联系人姓名:")
            success, msg = address_book.delete_contact(name)
            print(f"{'✅' if success else '❌'} {msg}")

        # 6. 退出系统
        elif choice == "6":
            if get_confirm("⚠️  确定要退出通讯录吗?(y/n):"):
                print("🙏 感谢使用,再见!")
                break

if __name__ == "__main__":
    main()

四、核心逻辑解析:吃透面向对象与持久化

上述代码已完整实现思维导图"功能实现"模块的全部要求,具备通讯录全功能与稳定性。接下来拆解核心逻辑,确保你掌握每一个关键设计,真正理解"面向对象+持久化"的实战价值。

1. 面向对象封装核心

  • 实体类封装:Contact类仅负责存储联系人数据,通过to_dict()/from_dict()方法实现与JSON的适配(解决JSON无法序列化对象的问题),隔离数据存储与业务逻辑;

  • 业务类封装:AddressBook类整合"增删改查+读写文件"逻辑,对外提供统一接口(如add_contact、delete_contact),主程序无需关心内部实现,只需调用接口,降低耦合度;

  • 数据安全:新增/删除操作后若保存失败,执行回滚操作(如新增失败移除联系人对象),确保内存数据与文件数据一致,避免数据错乱。

2. JSON持久化核心逻辑

  • 加载流程:程序启动时,AddressBook初始化调用load_data(),检查文件是否存在→不存在则创建空文件→存在则读取并反序列化为Contact对象列表,覆盖异常场景;

  • 保存流程:每次增删改后调用save_data(),将Contact对象列表转为字典列表→序列化写入JSON文件,返回保存结果用于业务判断;

  • 格式适配:JSON仅支持字典、列表等基础类型,无法直接存储对象,因此通过to_dict()/from_dict()实现"对象→字典"的转换,这是面向对象项目持久化的常用技巧。

3. 异常处理设计

异常处理覆盖全场景,贴合思维导图"异常防护"要点,核心设计包括:

  • 文件操作异常:捕获FileNotFoundError(文件不存在)、PermissionError(权限不足)、JSONDecodeError(格式错误),分别给出提示并容错;

  • 输入校验异常:通过utils工具函数提前校验手机号、邮箱格式,避免非法数据进入业务逻辑;

  • 业务异常:处理"重复新增""修改不存在联系人"等场景,通过返回值告知用户,而非抛出异常,提升交互体验。

五、测试与优化:从可用到好用

代码实现后,需通过多场景测试排查问题,同时结合思维导图"优化拓展"模块要点,提供可选优化方向,让通讯录更完善。

1. 核心测试场景(必测)

  • 基础功能测试:新增合法/非法联系人、查询存在/不存在联系人、修改/删除联系人,验证功能正常与数据持久化(关闭程序重启后数据保留);

  • 异常场景测试:输入非11位手机号、非法邮箱,删除不存在的联系人,手动损坏JSON文件,测试程序容错能力;

  • 边界场景测试:通讯录为空时查询/删除,批量新增联系人后展示,修改时仅改电话/仅改邮箱,验证逻辑正确性。

2. 可选优化方向(贴合思维导图拓展)

  • 高级查询:支持按手机号模糊查询、按姓名首字母排序展示,提升查询效率;

  • 批量操作:新增批量导入(从CSV/Excel文件导入联系人)、批量删除功能,适配多联系人场景;

  • 数据加密:对JSON文件中的手机号、邮箱进行简单加密(如base64),提升数据安全性;

  • 界面优化:添加颜色高亮(使用colorama模块)、分页展示(联系人过多时分页),提升交互体验。

结尾:通讯录实战搞定,开启合规爬虫入门

今天的简易通讯录实战,咱们整合了面向对象编程、文件持久化、模块化架构、异常处理等全套前期技能,突破了猜数字游戏的临时存储局限,实现了"数据可长期保存、业务逻辑可扩展"的实用项目。通过本次实战,你不仅掌握了面向对象的封装思想与JSON持久化的核心技巧,更建立了"实体→业务→交互"的分层开发思维,为后续开发复杂项目打下了坚实基础。

下一章,咱们将进入"项目实战3:合规爬虫入门",这是一款融合网络请求、数据解析与持久化的实战项目,完全对齐思维导图"爬虫基础"模块要点。项目将使用requests模块获取网页数据,通过解析技术提取有效信息,再将数据存储到JSON/CSV文件中,同时掌握爬虫合规性要点(robots协议、请求频率控制)。这款项目将打通"数据获取→解析→存储"的全流程,让你学会从网络上主动获取有价值的数据,进一步提升Python综合应用能力。如果今天的面向对象封装、JSON持久化逻辑有疑问,随时告诉我,咱们可针对性拆解复盘!

相关推荐
乙酸氧铍2 小时前
手机使用 ZeroTermux 调用 python 编辑缩放图像
图像处理·python·智能手机·安卓·termux
逄逄不是胖胖2 小时前
《动手学深度学习》-52文本预处理实现
人工智能·pytorch·python·深度学习
MediaTea2 小时前
Python:_sentinel 命名约定
开发语言·python·sentinel
Pyeako2 小时前
opencv计算机视觉--图形透视(投影)变换&图形拼接
人工智能·python·opencv·计算机视觉·图片拼接·投影变换·图形透视变换
茉莉玫瑰花茶2 小时前
C++17 详细特性解析(中)
开发语言·c++
shehuiyuelaiyuehao2 小时前
String的杂七杂八方法
java·开发语言
开发者小天2 小时前
python返回随机数
开发语言·python
2 小时前
java关于时间类
java·开发语言
lly2024062 小时前
C 标准库 - <stdlib.h>
开发语言