
文章目录
📖 开篇导读
恭喜你走到了面向对象编程综合实战的课堂!前29课我们系统学习了Python基础语法、函数、模块、文件操作、异常处理、面向对象三大特性、魔术方法、设计模式以及类与类之间的关系。现在是时候将这些知识全部融合,开发一个完整的人物信息管理系统控制台项目了。
💡 工作场景:在实际项目中,你需要面对类似的需求:员工管理系统、学生信息管理、客户关系管理(CRM)等。这些系统的核心都是对“人物”及相关信息进行增删改查、关联管理、持久化存储。通过本项目,你将掌握如何从零开始设计一个中等规模的控制台应用,体会OOP设计在实际开发中的威力。
本项目将涵盖:
- 继承:不同角色(学生、教师、员工)继承自
Person基类 - 组合:部门、地址作为独立对象与人物组合
- 封装:使用属性访问控制和私有属性
- 多态:统一接口处理不同角色
- 文件持久化:保存和加载数据(JSON)
- 异常处理:增强程序健壮性
- 菜单交互:命令行界面
学完本课,你将具备独立开发小型管理系统的能力,为后续学习Web框架(Django/Flask)打下坚实基础。
🎯 学习目标
| 目标编号 | 具体掌握内容 | 对应面试/工作价值 |
|---|---|---|
| 1️⃣ | 独立完成一个多类的OOP系统设计 | 锻炼面向对象分析与设计能力 |
| 2️⃣ | 掌握继承层次的设计技巧 | 复用代码,扩展功能 |
| 3️⃣ | 熟练运用组合关系(部门、地址) | 降低耦合,提升灵活性 |
| 4️⃣ | 实现数据持久化(文件读写) | 保存系统状态 |
| 5️⃣ | 编写健壮的异常处理和输入验证 | 提升用户体验 |
| 6️⃣ | 综合运用列表、字典、文件等知识点 | 融会贯通 |
🔥 面试考点:本项目涵盖了OOP设计、继承、组合、文件IO、异常处理等,面试官可能会问“你如何设计一个员工管理系统?”“为什么这里使用组合而不是继承?”
📚 知识点理论精讲(设计阶段)
一、需求分析
我们要开发一个人物信息管理系统,支持两类角色:学生和教师。但为了扩展性,设计一个Person基类。系统功能包括:
- 添加人物(学生或教师)
- 删除人物(按ID)
- 修改人物信息(按ID)
- 查询人物(按ID或姓名模糊)
- 显示所有人物(可按类型过滤,按名称排序)
- 统计信息(各类别人数、平均年龄)
- 部门管理(添加、修改、删除部门,将人员分配到部门)
- 数据持久化(自动保存到文件,启动时加载)
二、类设计
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 关系分析
- 继承:
Student和Teacher继承Person,实现多态work()。 - 组合:
Person包含Address对象(同生共死,Address随Person销毁)。 - 聚合:
Department包含多个Person(人员可以离开部门,独立存在)。 - 关联:
Person关联Department(通过department属性)。
三、文件持久化
使用JSON格式,将Person和Department对象序列化为字典,保存到文件;加载时重建对象。
💻 代码实现(完整项目)
我们将代码分为多个文件,但为了教学,可以集中在一个文件中,但按类组织。
"""
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(),强制子类实现。体现了多态性。Student和Teacher扩展了各自属性,并实现work()方法。info_dict()和from_dict()支持序列化和工厂创建。
4.2 组合与聚合
- 组合:
Person包含Address对象,Address的生命周期完全由Person管理(随Person一起序列化)。 - 聚合:
Department包含members列表,人员可以离开部门独立存在。部门删除时人员只是失去关联,不会删除。
4.3 单例模式
PersonManager使用__new__实现单例,确保全局只有一个管理实例,避免数据不一致。
4.4 数据持久化
- 使用JSON格式保存
persons和departments。 - 重建数据时,先加载部门(但成员ID暂存),再加载人员(此时部门映射已有),最后恢复部门对人员的引用。这解决了循环引用问题。
4.5 异常处理与输入验证
- 输入函数
input_int、input_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) |
| 7 | ID自动生成与加载冲突 | 加载时更新_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的主要方法编写简单的单元测试(使用unittest或pytest),测试添加、删除、查找等。
第7题:图形界面扩展(选做)
使用tkinter或PyQt为该系统开发一个简单的GUI界面,调用相同的PersonManager核心逻辑。
🧠 知识点思维导图总结
🔜 下节课预告
本项目是基础语法和面向对象编程的综合演练。从下一节课开始,我们将进入Python高级特性:迭代器、生成器、装饰器、并发编程等,让你的代码更高效、更优雅。
第31课:迭代器、生成器、yield底层原理与实战精讲
内容包括:
- 迭代协议(
__iter__、__next__) - 生成器函数与
yield语句 - 生成器表达式
- 惰性求值与内存优化
- 实战:大数据处理、无限序列
迭代器和生成器是Python高效处理数据流的利器,学完你将写出内存友好的代码。
🌟 学习鼓励:完成这个综合项目,意味着你已经具备了独立开发控制台应用程序的能力。不要停止在这里,尝试扩展它、添加新功能、甚至写一个GUI版本。每一行代码都是你成为专业程序员的积累。继续保持热情,下一阶段的进阶内容更加精彩!
🔗《50节课 Python 从入门到精通》系列课程导航
🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~
316

被折叠的 条评论
为什么被折叠?



