第30课:Python|OOP综合实战【人物信息管理系统控制台完整项目开发】

在这里插入图片描述


📖 开篇导读

恭喜你走到了面向对象编程综合实战的课堂!前29课我们系统学习了Python基础语法、函数、模块、文件操作、异常处理、面向对象三大特性、魔术方法、设计模式以及类与类之间的关系。现在是时候将这些知识全部融合,开发一个完整的人物信息管理系统控制台项目了。

💡 工作场景:在实际项目中,你需要面对类似的需求:员工管理系统、学生信息管理、客户关系管理(CRM)等。这些系统的核心都是对“人物”及相关信息进行增删改查、关联管理、持久化存储。通过本项目,你将掌握如何从零开始设计一个中等规模的控制台应用,体会OOP设计在实际开发中的威力。

本项目将涵盖:

  • 继承:不同角色(学生、教师、员工)继承自Person基类
  • 组合:部门、地址作为独立对象与人物组合
  • 封装:使用属性访问控制和私有属性
  • 多态:统一接口处理不同角色
  • 文件持久化:保存和加载数据(JSON)
  • 异常处理:增强程序健壮性
  • 菜单交互:命令行界面

学完本课,你将具备独立开发小型管理系统的能力,为后续学习Web框架(Django/Flask)打下坚实基础。


🎯 学习目标

目标编号具体掌握内容对应面试/工作价值
1️⃣独立完成一个多类的OOP系统设计锻炼面向对象分析与设计能力
2️⃣掌握继承层次的设计技巧复用代码,扩展功能
3️⃣熟练运用组合关系(部门、地址)降低耦合,提升灵活性
4️⃣实现数据持久化(文件读写)保存系统状态
5️⃣编写健壮的异常处理和输入验证提升用户体验
6️⃣综合运用列表、字典、文件等知识点融会贯通

🔥 面试考点:本项目涵盖了OOP设计、继承、组合、文件IO、异常处理等,面试官可能会问“你如何设计一个员工管理系统?”“为什么这里使用组合而不是继承?”


📚 知识点理论精讲(设计阶段)

一、需求分析

我们要开发一个人物信息管理系统,支持两类角色:学生和教师。但为了扩展性,设计一个Person基类。系统功能包括:

  1. 添加人物(学生或教师)
  2. 删除人物(按ID)
  3. 修改人物信息(按ID)
  4. 查询人物(按ID或姓名模糊)
  5. 显示所有人物(可按类型过滤,按名称排序)
  6. 统计信息(各类别人数、平均年龄)
  7. 部门管理(添加、修改、删除部门,将人员分配到部门)
  8. 数据持久化(自动保存到文件,启动时加载)

二、类设计

2.1 类图

Person (抽象基类)
├── id (int)
├── name (str)
├── age (int)
├── address (Address对象,组合)
├── department (Department对象,关联)
├── __init__()
├── work() (抽象方法,多态)
├── info() (返回字典)
└── from_dict() (类方法)

Student(Person)
├── grade (str, 年级)
├── scores (list, 成绩列表)
├── work() -> "学习"

Teacher(Person)
├── subject (str, 所教科目)
├── salary (float)
├── work() -> "教学"

Address (独立类)
├── city, street, zipcode

Department (独立类)
├── dept_id, name
├── manager (Person, 关联)
├── members (list, 人员列表,聚合)
├── add_member(), remove_member()

PersonManager (核心管理类,单例?)
├── persons (dict, id -> Person)
├── departments (dict, dept_id -> Department)
├── next_id (类属性)
├── add_person(), remove_person(), find_person()
├── add_department(), ...
├── save_data(), load_data()

2.2 关系分析

  • 继承StudentTeacher继承Person,实现多态work()
  • 组合Person包含Address对象(同生共死,Address随Person销毁)。
  • 聚合Department包含多个Person(人员可以离开部门,独立存在)。
  • 关联Person关联Department(通过department属性)。

三、文件持久化

使用JSON格式,将PersonDepartment对象序列化为字典,保存到文件;加载时重建对象。


💻 代码实现(完整项目)

我们将代码分为多个文件,但为了教学,可以集中在一个文件中,但按类组织。

"""
person_management_system.py
人物信息管理系统 - OOP综合实战
包含:Person基类、Student、Teacher、Address、Department、PersonManager
功能:增删改查、部门管理、文件持久化
"""

import json
import os
from abc import ABC, abstractmethod
from typing import Dict, List, Optional

# ======================== 辅助常量 ========================
DATA_FILE = "pms_data.json"

# ======================== Address 类(组合组件)======================
class Address:
    """地址类,用于组合到Person中"""
    def __init__(self, city: str = "", street: str = "", zipcode: str = ""):
        self.city = city
        self.street = street
        self.zipcode = zipcode
    
    def __str__(self):
        return f"{self.city} {self.street} {self.zipcode}".strip()
    
    def to_dict(self):
        return {"city": self.city, "street": self.street, "zipcode": self.zipcode}
    
    @classmethod
    def from_dict(cls, data):
        return cls(data.get("city", ""), data.get("street", ""), data.get("zipcode", ""))

# ======================== Department 类(聚合关系)======================
class Department:
    """部门类,与Person是聚合关系(人员可独立于部门)"""
    def __init__(self, dept_id: str, name: str):
        self.dept_id = dept_id
        self.name = name
        self.members: List['Person'] = []   # 聚合:部门包含人员引用
    
    def add_member(self, person: 'Person'):
        if person not in self.members:
            self.members.append(person)
            person.department = self   # 双向关联方便查询
    
    def remove_member(self, person: 'Person'):
        if person in self.members:
            self.members.remove(person)
            person.department = None
    
    def get_member_names(self):
        return [p.name for p in self.members]
    
    def to_dict(self):
        return {
            "dept_id": self.dept_id,
            "name": self.name,
            "member_ids": [p.person_id for p in self.members]
        }
    
    @classmethod
    def from_dict(cls, data, persons_map):
        dept = cls(data["dept_id"], data["name"])
        # 成员稍后通过persons_map重建关联(在加载完所有Person后)
        dept._member_ids = data.get("member_ids", [])
        return dept
    
    def restore_members(self, persons_map):
        for pid in getattr(self, "_member_ids", []):
            if pid in persons_map:
                self.add_member(persons_map[pid])
        if hasattr(self, "_member_ids"):
            delattr(self, "_member_ids")
    
    def __str__(self):
        return f"{self.name} ({len(self.members)}人)"

# ======================== Person 抽象基类 ========================
class Person(ABC):
    """人物抽象基类,所有具体角色的父类"""
    _next_id = 1   # 类属性,自动生成ID
    
    def __init__(self, name: str, age: int, address: Address = None):
        self._person_id = Person._next_id
        Person._next_id += 1
        self.name = name
        self.age = age
        self.address = address if address else Address()
        self.department: Optional[Department] = None   # 关联部门
    
    @property
    def person_id(self):
        return self._person_id
    
    @abstractmethod
    def work(self) -> str:
        """抽象方法,子类实现各自的工作行为"""
        pass
    
    def info_dict(self) -> dict:
        """转换为字典用于序列化"""
        return {
            "person_id": self.person_id,
            "type": self.__class__.__name__,
            "name": self.name,
            "age": self.age,
            "address": self.address.to_dict(),
            "department_id": self.department.dept_id if self.department else None
        }
    
    @classmethod
    def from_dict(cls, data, dept_map):
        """工厂方法:根据type创建对应的子类实例"""
        person_type = data["type"]
        address = Address.from_dict(data.get("address", {}))
        if person_type == "Student":
            person = Student(data["name"], data["age"], address, data.get("grade", ""))
            person.scores = data.get("scores", [])
        elif person_type == "Teacher":
            person = Teacher(data["name"], data["age"], address, data.get("subject", ""), data.get("salary", 0.0))
        else:
            raise ValueError(f"未知类型: {person_type}")
        # 注意:person_id需要恢复原来的值,而不是重新生成
        person._person_id = data["person_id"]
        # 更新全局next_id避免冲突
        if person._person_id >= cls._next_id:
            cls._next_id = person._person_id + 1
        # 部门关联稍后处理
        dept_id = data.get("department_id")
        if dept_id and dept_id in dept_map:
            person.department = dept_map[dept_id]
        return person
    
    def __str__(self):
        dept_name = self.department.name if self.department else "无部门"
        return f"[{self.person_id}] {self.name} ({self.age}岁) 部门:{dept_name} 地址:{self.address}"

# ======================== Student 类 ========================
class Student(Person):
    def __init__(self, name: str, age: int, address: Address = None, grade: str = ""):
        super().__init__(name, age, address)
        self.grade = grade   # 年级
        self.scores = []     # 成绩列表
    
    def work(self) -> str:
        return f"{self.name} 正在学习"
    
    def add_score(self, score: float):
        if 0 <= score <= 100:
            self.scores.append(score)
        else:
            raise ValueError("成绩范围0-100")
    
    def avg_score(self):
        return sum(self.scores) / len(self.scores) if self.scores else 0.0
    
    def info_dict(self):
        data = super().info_dict()
        data.update({"grade": self.grade, "scores": self.scores})
        return data
    
    def __str__(self):
        base = super().__str__()
        return f"{base} 学生 年级:{self.grade} 平均分:{self.avg_score():.2f}"

# ======================== Teacher 类 ========================
class Teacher(Person):
    def __init__(self, name: str, age: int, address: Address = None, subject: str = "", salary: float = 0.0):
        super().__init__(name, age, address)
        self.subject = subject
        self.salary = salary
    
    def work(self) -> str:
        return f"{self.name} 正在教 {self.subject}"
    
    def info_dict(self):
        data = super().info_dict()
        data.update({"subject": self.subject, "salary": self.salary})
        return data
    
    def __str__(self):
        base = super().__str__()
        return f"{base} 教师 科目:{self.subject} 工资:{self.salary}"

# ======================== PersonManager 核心管理类 ========================
class PersonManager:
    """单例模式管理所有人物和部门"""
    _instance = None
    
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._initialized = False
        return cls._instance
    
    def __init__(self):
        if self._initialized:
            return
        self._initialized = True
        self.persons: Dict[int, Person] = {}      # id -> Person
        self.departments: Dict[str, Department] = {}  # dept_id -> Department
        self._load_data()   # 启动时加载数据
    
    # ---------- 人物操作 ----------
    def add_person(self, person: Person):
        if person.person_id in self.persons:
            raise ValueError(f"ID {person.person_id} 已存在")
        self.persons[person.person_id] = person
        self._save_data()
        print(f"添加成功: {person}")
    
    def remove_person(self, person_id: int):
        if person_id not in self.persons:
            print("人员不存在")
            return
        person = self.persons[person_id]
        # 如果属于某个部门,从部门中移除
        if person.department:
            person.department.remove_member(person)
        del self.persons[person_id]
        self._save_data()
        print(f"已删除人员ID {person_id}")
    
    def find_person_by_id(self, person_id: int) -> Optional[Person]:
        return self.persons.get(person_id)
    
    def find_persons_by_name(self, name_keyword: str) -> List[Person]:
        keyword = name_keyword.lower()
        return [p for p in self.persons.values() if keyword in p.name.lower()]
    
    def list_all_persons(self, sort_by: str = "id", person_type: str = None):
        """列出所有人员,可按类型过滤,按id或name排序"""
        result = list(self.persons.values())
        if person_type:
            result = [p for p in result if p.__class__.__name__.lower() == person_type.lower()]
        if sort_by == "id":
            result.sort(key=lambda p: p.person_id)
        elif sort_by == "name":
            result.sort(key=lambda p: p.name)
        return result
    
    # ---------- 部门操作 ----------
    def add_department(self, dept_id: str, name: str):
        if dept_id in self.departments:
            raise ValueError(f"部门ID {dept_id} 已存在")
        dept = Department(dept_id, name)
        self.departments[dept_id] = dept
        self._save_data()
        print(f"部门添加成功: {dept.name}")
    
    def remove_department(self, dept_id: str):
        if dept_id not in self.departments:
            print("部门不存在")
            return
        dept = self.departments[dept_id]
        # 将部门中成员移出(部门解散,成员独立)
        for member in dept.members[:]:
            dept.remove_member(member)
        del self.departments[dept_id]
        self._save_data()
        print(f"部门 {dept.name} 已删除")
    
    def assign_person_to_dept(self, person_id: int, dept_id: str):
        person = self.find_person_by_id(person_id)
        if not person:
            print("人员不存在")
            return
        dept = self.departments.get(dept_id)
        if not dept:
            print("部门不存在")
            return
        # 先从原部门移除
        if person.department:
            person.department.remove_member(person)
        dept.add_member(person)
        self._save_data()
        print(f"{person.name} 已分配到 {dept.name}")
    
    # ---------- 统计信息 ----------
    def statistics(self):
        print("\n========== 系统统计 ==========")
        total = len(self.persons)
        students = sum(1 for p in self.persons.values() if isinstance(p, Student))
        teachers = total - students
        print(f"总人数: {total} (学生:{students}, 教师:{teachers})")
        avg_age = sum(p.age for p in self.persons.values()) / total if total else 0
        print(f"平均年龄: {avg_age:.2f}")
        print(f"部门数量: {len(self.departments)}")
        for dept in self.departments.values():
            print(f"  {dept.name}: {len(dept.members)}人")
    
    # ---------- 数据持久化 ----------
    def _save_data(self):
        """将 persons 和 departments 保存到JSON文件"""
        data = {
            "persons": [p.info_dict() for p in self.persons.values()],
            "departments": [d.to_dict() for d in self.departments.values()],
            "next_id": Person._next_id
        }
        try:
            with open(DATA_FILE, "w", encoding="utf-8") as f:
                json.dump(data, f, indent=4, ensure_ascii=False)
        except Exception as e:
            print(f"保存数据失败: {e}")
    
    def _load_data(self):
        """从JSON文件加载数据并重建对象"""
        if not os.path.exists(DATA_FILE):
            print("未找到数据文件,启动空系统")
            return
        try:
            with open(DATA_FILE, "r", encoding="utf-8") as f:
                data = json.load(f)
        except Exception as e:
            print(f"加载数据失败: {e}")
            return
        
        # 先重建部门(但成员ID列表暂存)
        dept_map = {}
        for ddata in data.get("departments", []):
            dept = Department.from_dict(ddata, {})
            dept_map[dept.dept_id] = dept
            self.departments[dept.dept_id] = dept
        
        # 重建人员(传递部门映射)
        Person._next_id = data.get("next_id", 1)
        for pdata in data.get("persons", []):
            try:
                person = Person.from_dict(pdata, dept_map)
                self.persons[person.person_id] = person
            except Exception as e:
                print(f"重建人员失败: {e}")
        
        # 恢复部门中的成员引用(因为Person已重建)
        for dept in self.departments.values():
            dept.restore_members(self.persons)
        
        print(f"加载完成: {len(self.persons)}人, {len(self.departments)}个部门")

# ======================== 命令行交互界面 ========================
def input_int(prompt, min_val=None, max_val=None):
    while True:
        try:
            val = int(input(prompt))
            if min_val is not None and val < min_val:
                print(f"输入不能小于{min_val}")
                continue
            if max_val is not None and val > max_val:
                print(f"输入不能大于{max_val}")
                continue
            return val
        except ValueError:
            print("请输入整数")

def input_float(prompt):
    while True:
        try:
            return float(input(prompt))
        except ValueError:
            print("请输入数字")

def create_person_from_input():
    """交互创建人物"""
    print("请选择类型: 1.学生 2.教师")
    typ = input("请输入: ").strip()
    name = input("姓名: ").strip()
    age = input_int("年龄: ", 0, 150)
    city = input("城市: ")
    street = input("街道: ")
    zipcode = input("邮编: ")
    addr = Address(city, street, zipcode)
    if typ == "1":
        grade = input("年级: ")
        stu = Student(name, age, addr, grade)
        # 可选输入成绩
        while True:
            sc = input("添加成绩(直接回车结束): ").strip()
            if sc == "":
                break
            try:
                score = float(sc)
                stu.add_score(score)
            except ValueError:
                print("成绩无效")
        return stu
    elif typ == "2":
        subject = input("所教科目: ")
        salary = input_float("工资: ")
        return Teacher(name, age, addr, subject, salary)
    else:
        print("无效类型")
        return None

def main_menu():
    pm = PersonManager()
    while True:
        print("\n" + "="*50)
        print("       人物信息管理系统 v2.0")
        print("="*50)
        print("1. 添加人物")
        print("2. 删除人物")
        print("3. 修改人物(暂未实现,可删除后重加)")
        print("4. 查询人物")
        print("5. 显示所有人物")
        print("6. 统计信息")
        print("7. 部门管理")
        print("8. 分配人员到部门")
        print("0. 退出")
        choice = input("请选择: ").strip()
        
        if choice == "1":
            p = create_person_from_input()
            if p:
                pm.add_person(p)
        elif choice == "2":
            pid = input_int("请输入要删除的人员ID: ")
            pm.remove_person(pid)
        elif choice == "3":
            print("修改功能扩展:可先删除再添加,或自行实现update方法")
            # 扩展点:可以实现人员编辑
        elif choice == "4":
            print("1. 按ID精确查询")
            print("2. 按姓名模糊查询")
            sub = input("选择: ")
            if sub == "1":
                pid = input_int("ID: ")
                p = pm.find_person_by_id(pid)
                if p:
                    print(p)
                else:
                    print("未找到")
            elif sub == "2":
                kw = input("姓名关键词: ")
                results = pm.find_persons_by_name(kw)
                if results:
                    for r in results:
                        print(r)
                else:
                    print("未找到")
        elif choice == "5":
            print("1. 按ID排序  2. 按姓名排序  3. 仅显示学生  4. 仅显示教师")
            sub = input("选择: ")
            sort_by = "id" if sub == "1" else "name"
            ptype = None
            if sub == "3":
                ptype = "student"
            elif sub == "4":
                ptype = "teacher"
            persons = pm.list_all_persons(sort_by=sort_by, person_type=ptype)
            if not persons:
                print("无数据")
            else:
                for p in persons:
                    print(p)
        elif choice == "6":
            pm.statistics()
        elif choice == "7":
            print("\n--- 部门管理 ---")
            print("1. 添加部门  2. 删除部门  3. 列出所有部门")
            sub = input("选择: ")
            if sub == "1":
                did = input("部门ID(字符串): ").strip()
                name = input("部门名称: ").strip()
                try:
                    pm.add_department(did, name)
                except ValueError as e:
                    print(e)
            elif sub == "2":
                did = input("部门ID: ").strip()
                pm.remove_department(did)
            elif sub == "3":
                for d in pm.departments.values():
                    print(f"{d.dept_id}: {d.name} ({len(d.members)}人)")
        elif choice == "8":
            pid = input_int("人员ID: ")
            did = input("部门ID: ").strip()
            pm.assign_person_to_dept(pid, did)
        elif choice == "0":
            print("退出系统")
            break
        else:
            print("无效选项")

if __name__ == "__main__":
    main_menu()

四、代码说明与重点解析

4.1 抽象基类与继承

  • Person继承ABC,定义抽象方法work(),强制子类实现。体现了多态性。
  • StudentTeacher扩展了各自属性,并实现work()方法。
  • info_dict()from_dict()支持序列化和工厂创建。

4.2 组合与聚合

  • 组合Person包含Address对象,Address的生命周期完全由Person管理(随Person一起序列化)。
  • 聚合Department包含members列表,人员可以离开部门独立存在。部门删除时人员只是失去关联,不会删除。

4.3 单例模式

PersonManager使用__new__实现单例,确保全局只有一个管理实例,避免数据不一致。

4.4 数据持久化

  • 使用JSON格式保存personsdepartments
  • 重建数据时,先加载部门(但成员ID暂存),再加载人员(此时部门映射已有),最后恢复部门对人员的引用。这解决了循环引用问题。

4.5 异常处理与输入验证

  • 输入函数input_intinput_float循环验证。
  • 文件读写使用try-except
  • 添加部门时检查ID重复。

⚠️ 易错点避坑总结(针对本项目)

序号坑点解决方案
1序列化时循环引用(人员<->部门)存储时只存部门ID和人员ID列表,加载时二次关联
2继承类序列化时需要保留具体类型在字典中加入type字段,使用工厂方法重建
3单例模式中__init__多次执行使用标志位_initialized
4删除人员时忘记从部门中移除remove_person中检查person.department并调用remove_member
5输入年龄、成绩等未做边界验证使用input_int带min/max
6文件保存时未处理中文编码json.dump(ensure_ascii=False)
7ID自动生成与加载冲突加载时更新_next_id,确保新ID不冲突
8部门添加成员时双向关联不一致add_member中同时设置person.department = self

📝 课后实战练习题

第1题:扩展角色

增加新的角色Staff(行政人员),属性position职位。在PersonManager中支持添加和显示。

第2题:增加修改功能

实现modify_person方法,允许修改姓名、年龄、地址、年级/科目等。提示:可通过ID找到原对象,用新输入更新属性。

第3题:数据导出为CSV

编写函数导出所有人员信息到CSV文件,包括姓名、类型、年龄、部门、成绩/科目等信息。

第4题:部门经理

Department增加manager属性(Person对象),并在部门管理菜单中支持设置经理。注意经理可能不是部门成员。

第5题:搜索优化

实现按年龄范围、按成绩范围(学生)搜索的功能。

第6题:单元测试

PersonManager的主要方法编写简单的单元测试(使用unittestpytest),测试添加、删除、查找等。

第7题:图形界面扩展(选做)

使用tkinterPyQt为该系统开发一个简单的GUI界面,调用相同的PersonManager核心逻辑。


🧠 知识点思维导图总结

第30课:OOP综合实战

项目架构

抽象基类 Person

Student Teacher 继承

Address 组合

Department 聚合

PersonManager 单例

核心功能

增删改查

部门分配

统计

文件持久化

关键技术

继承多态

组合聚合

异常处理

JSON序列化

工厂方法

交互界面

命令行菜单

输入验证

面试考点

为什么用抽象基类

组合与聚合区别

单例模式实现

循环引用序列化解决


🔜 下节课预告

本项目是基础语法和面向对象编程的综合演练。从下一节课开始,我们将进入Python高级特性:迭代器、生成器、装饰器、并发编程等,让你的代码更高效、更优雅。

第31课:迭代器、生成器、yield底层原理与实战精讲

内容包括:

  • 迭代协议(__iter____next__
  • 生成器函数与yield语句
  • 生成器表达式
  • 惰性求值与内存优化
  • 实战:大数据处理、无限序列

迭代器和生成器是Python高效处理数据流的利器,学完你将写出内存友好的代码。

🌟 学习鼓励:完成这个综合项目,意味着你已经具备了独立开发控制台应用程序的能力。不要停止在这里,尝试扩展它、添加新功能、甚至写一个GUI版本。每一行代码都是你成为专业程序员的积累。继续保持热情,下一阶段的进阶内容更加精彩!


🔗《50节课 Python 从入门到精通》系列课程导航

去订阅

🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Thomas.Sir

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值