Python: 实战:图书管理系统
这是 Phase 4 的结业项目。把前几课学的类、继承、封装、魔术方法、文件操作全部串起来,写一个真正可以用的图书管理系统。完成这个项目,你就真正掌握了 Python 面向对象编程。
1. 项目需求
TEXT
📖 仅展示
1. 图书管理(CRUD)
├── 添加图书(标题、作者、ISBN、出版年份)
├── 删除图书(按 ISBN)
├── 修改图书信息
└── 查询图书(按标题模糊搜索 / 按作者搜索)
2. 借阅管理
├── 借书(记录借书人、借书日期)
├── 还书(计算是否逾期)
└── 查看借阅记录
3. 数据统计
├── 图书总数
├── 在馆/借出数量
└── 按作者统计
4. 数据持久化(CSV 文件保存,启动时自动加载)
2. 完整代码
▶ 示例:图书管理系统
TEXT
📖 仅展示
import csv
import os
from datetime import datetime, timedelta
from dataclasses import dataclass
# ====== 数据模型 ======
@dataclass
class Book:
"""图书数据类"""
title: str
author: str
isbn: str
year: int
is_borrowed: bool = False
borrower: str = ""
borrow_date: str = ""
def to_csv_row(self):
"""转为 CSV 行"""
return [self.title, self.author, self.isbn, str(self.year),
str(self.is_borrowed), self.borrower, self.borrow_date]
@staticmethod
def from_csv_row(row):
"""从 CSV 行创建 Book 对象"""
return Book(
title=row[0], author=row[1], isbn=row[2],
year=int(row[3]),
is_borrowed=row[4] == "True",
borrower=row[5], borrow_date=row[6]
)
# ====== 业务逻辑层 ======
class Library:
"""图书馆管理系统"""
DATA_FILE = "library_data.csv"
def __init__(self):
self.books = []
self.load_data()
# ---- 持久化 ----
def load_data(self):
"""从 CSV 加载数据"""
if not os.path.exists(self.DATA_FILE):
return
with open(self.DATA_FILE, "r", encoding="utf-8") as f:
reader = csv.reader(f)
next(reader, None) # 跳过表头
for row in reader:
if row:
self.books.append(Book.from_csv_row(row))
print(f"已加载 {len(self.books)} 本图书数据。")
def save_data(self):
"""保存数据到 CSV"""
with open(self.DATA_FILE, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["标题", "作者", "ISBN", "出版年份",
"已借出", "借书人", "借书日期"])
for book in self.books:
writer.writerow(book.to_csv_row())
# ---- 图书管理 ----
def add_book(self, title, author, isbn, year):
"""添加图书"""
for book in self.books:
if book.isbn == isbn:
return False, "ISBN 已存在!"
self.books.append(Book(title, author, isbn, year))
self.save_data()
return True, f"已添加《{title}》"
def delete_book(self, isbn):
"""删除图书"""
for i, book in enumerate(self.books):
if book.isbn == isbn:
removed = self.books.pop(i)
self.save_data()
return True, f"已删除《{removed.title}》"
return False, "未找到该 ISBN 的图书。"
def search_by_title(self, keyword):
"""按标题模糊搜索"""
return [b for b in self.books if keyword.lower() in b.title.lower()]
def search_by_author(self, author):
"""按作者搜索"""
return [b for b in self.books if author.lower() in b.author.lower()]
def get_book_by_isbn(self, isbn):
"""按 ISBN 精确查找"""
for book in self.books:
if book.isbn == isbn:
return book
return None
# ---- 借阅管理 ----
def borrow_book(self, isbn, borrower):
"""借书"""
book = self.get_book_by_isbn(isbn)
if not book:
return False, "未找到该图书。"
if book.is_borrowed:
return False, f"《{book.title}》已被 {book.borrower} 借出。"
book.is_borrowed = True
book.borrower = borrower
book.borrow_date = datetime.now().strftime("%Y-%m-%d")
self.save_data()
return True, f"《{book.title}》已由 {borrower} 借出。"
def return_book(self, isbn):
"""还书"""
book = self.get_book_by_isbn(isbn)
if not book:
return False, "未找到该图书。"
if not book.is_borrowed:
return False, f"《{book.title}》没有被借出。"
# 计算是否逾期(借阅 30 天)
borrow_date = datetime.strptime(book.borrow_date, "%Y-%m-%d")
days_borrowed = (datetime.now() - borrow_date).days
is_overdue = days_borrowed > 30
book.is_borrowed = False
book.borrower = ""
book.borrow_date = ""
self.save_data()
if is_overdue:
return True, f"《{book.title}》已还,逾期 {days_borrowed - 30} 天。"
return True, f"《{book.title}》已还,借阅 {days_borrowed} 天。"
# ---- 统计 ----
def get_stats(self):
"""获取统计信息"""
total = len(self.books)
borrowed = sum(1 for b in self.books if b.is_borrowed)
available = total - borrowed
# 按作者统计
author_count = {}
for book in self.books:
author_count[book.author] = author_count.get(book.author, 0) + 1
return {
"total": total,
"available": available,
"borrowed": borrowed,
"by_author": dict(sorted(author_count.items(),
key=lambda x: x[1], reverse=True))
}
# ====== 展示层 ======
def show_menu():
print("\n" + "=" * 40)
print("📚 图书管理系统")
print("=" * 40)
print("1. 添加图书")
print("2. 删除图书")
print("3. 搜索图书")
print("4. 显示所有图书")
print("5. 借书")
print("6. 还书")
print("7. 数据统计")
print("8. 退出")
print("=" * 40)
def display_books(books, title="搜索结果"):
"""显示图书列表"""
if not books:
print(f"\n{title}:无记录。")
return
print(f"\n--- {title} ---")
print(f"{'书名':<20}{'作者':<10}{'ISBN':<15}{'状态':<8}")
print("-" * 53)
for b in books:
status = "已借出" if b.is_borrowed else "在馆"
print(f"{b.title:<20}{b.author:<10}{b.isbn:<15}{status:<8}")
def main():
library = Library()
while True:
show_menu()
choice = input("请选择操作 (1-8):").strip()
if choice == "1":
title = input("书名:").strip()
author = input("作者:").strip()
isbn = input("ISBN:").strip()
year = input("出版年份:").strip()
success, msg = library.add_book(title, author, isbn, int(year))
print(f" {'✅' if success else '❌'} {msg}")
elif choice == "2":
isbn = input("请输入要删除的 ISBN:").strip()
msg = library.delete_book(isbn)
print(f" {'✅' if msg else '❌'} {msg}")
elif choice == "3":
print(" 1. 按标题搜索")
print(" 2. 按作者搜索")
opt = input("请选择:").strip()
if opt == "1":
keyword = input("请输入书名关键词:").strip()
results = library.search_by_title(keyword)
elif opt == "2":
author = input("请输入作者名:").strip()
results = library.search_by_author(author)
else:
print("无效选择")
continue
display_books(results)
elif choice == "4":
display_books(library.books, "所有图书")
elif choice == "5":
isbn = input("请输入要借的图书 ISBN:").strip()
borrower = input("借书人姓名:").strip()
success, msg = library.borrow_book(isbn, borrower)
print(f" {'✅' if success else '❌'} {msg}")
elif choice == "6":
isbn = input("请输入要还的图书 ISBN:").strip()
success, msg = library.return_book(isbn)
print(f" {'✅' if success else '❌'} {msg}")
elif choice == "7":
stats = library.get_stats()
print(f"\n--- 数据统计 ---")
print(f"图书总数:{stats['total']}")
print(f"在馆:{stats['available']} 借出:{stats['borrowed']}")
print(f"\n按作者统计:")
for author, count in stats['by_author'].items():
print(f" {author}:{count} 本")
elif choice == "8":
print("感谢使用,再见!👋")
break
else:
print("无效选择,请输入 1-8。")
if __name__ == "__main__":
main()
▶ 示例:用 unittest 测试 Library 类
写完 260 行的图书管理系统,怎么证明它真的能用?答:用单元测试。
Python 内置的 unittest 框架可以自动化验证每个方法的行为。
TEXT
📖 仅展示
import unittest
import os
from library_project import Library # 把上一节代码保存为 library_project.py
class TestLibrary(unittest.TestCase):
"""测试 Library 类的核心方法"""
def setUp(self):
"""每个测试方法前都执行:准备干净环境"""
self.test_file = "test_library_data.csv"
Library.DATA_FILE = self.test_file # 临时改数据文件,避免污染
if os.path.exists(self.test_file):
os.remove(self.test_file)
self.lib = Library()
def tearDown(self):
"""每个测试方法后都执行:清理"""
if os.path.exists(self.test_file):
os.remove(self.test_file)
# ---- 测试添加图书 ----
def test_add_book_success(self):
"""测试:成功添加一本新书"""
success, msg = self.lib.add_book("Python 编程", "Eric", "ISBN001", 2024)
self.assertTrue(success)
self.assertIn("已添加", msg)
self.assertEqual(len(self.lib.books), 1)
def test_add_book_duplicate_isbn(self):
"""测试:重复 ISBN 不能添加"""
self.lib.add_book("Python 编程", "Eric", "ISBN001", 2024)
success, msg = self.lib.add_book("Python 入门", "Bob", "ISBN001", 2023)
self.assertFalse(success)
self.assertIn("ISBN 已存在", msg)
self.assertEqual(len(self.lib.books), 1) # 没加进去
# ---- 测试借还书 ----
def test_borrow_and_return(self):
"""测试:借书成功 → 还书成功"""
self.lib.add_book("Python 编程", "Eric", "ISBN001", 2024)
success, msg = self.lib.borrow_book("ISBN001", "Alice")
self.assertTrue(success)
self.assertIn("已由 Alice 借出", msg)
book = self.lib.get_book_by_isbn("ISBN001")
self.assertTrue(book.is_borrowed)
self.assertEqual(book.borrower, "Alice")
# 还书
success, msg = self.lib.return_book("ISBN001")
self.assertTrue(success)
self.assertFalse(book.is_borrowed)
def test_borrow_unavailable_book(self):
"""测试:已借出的书不能再借"""
self.lib.add_book("Python 编程", "Eric", "ISBN001", 2024)
self.lib.borrow_book("ISBN001", "Alice")
success, msg = self.lib.borrow_book("ISBN001", "Bob")
self.assertFalse(success)
self.assertIn("已被 Alice 借出", msg)
# ---- 测试搜索 ----
def test_search_by_title(self):
"""测试:标题模糊搜索"""
self.lib.add_book("Python 编程", "Eric", "001", 2024)
self.lib.add_book("Java 入门", "Bob", "002", 2023)
self.lib.add_book("Python 进阶", "Alice", "003", 2024)
results = self.lib.search_by_title("python")
self.assertEqual(len(results), 2) # 命中 2 本含 "python" 的
if __name__ == "__main__":
unittest.main(verbosity=2)
运行命令:
python -m unittest test_library.py -v
输出:
TEXT
📖 仅展示
test_add_book_duplicate_isbn ... ok
test_add_book_success ... ok
test_borrow_and_return ... ok
test_borrow_unavailable_book ... ok
test_search_by_title ... ok
----------------------------------------------------------------------
Ran 5 tests in 0.003s
OK
💡 提示:5 个测试 3 毫秒跑完——这就是"用代码验证代码"的力量。以后改
Library 类时,跑一遍这个测试就知道有没有破坏老功能。setUp / tearDown 让每个测试都在干净环境跑,互不干扰。
▶ 示例:图书搜索与筛选(难度⭐)
PYTHON
books = [
{"title": "Python编程", "author": "Eric", "isbn": "001", "category": "科技"},
{"title": "Python进阶", "author": "Alice", "isbn": "002", "category": "科技"},
{"title": "百年孤独", "author": "Marquez", "isbn": "003", "category": "文学"},
{"title": "数据结构", "author": "Bob", "isbn": "004", "category": "科技"},
{"title": "红楼梦", "author": "曹雪芹", "isbn": "005", "category": "文学"},
]
def search_books(books, keyword=None, category=None, author=None):
"""多条件搜索图书"""
results = books
if keyword:
results = [b for b in results if keyword.lower() in b["title"].lower()]
if category:
results = [b for b in results if b["category"] == category]
if author:
results = [b for b in results if author.lower() in b["author"].lower()]
return results
print("=== 搜索关键词 python ===")
for b in search_books(books, keyword="python"):
print(f" 《{b['title']}》 - {b['author']}")
print("\n=== 筛选分类:文学 ===")
for b in search_books(books, category="文学"):
print(f" 《{b['title']}》 - {b['author']}")
print("\n=== 组合搜索:科技类 + python ===")
for b in search_books(books, keyword="python", category="科技"):
print(f" 《{b['title']}》 - {b['author']}")
输出:
TEXT
📖 仅展示
=== 搜索关键词 python ===
《Python编程》 - Eric
《Python进阶》 - Alice
=== 筛选分类:文学 ===
《百年孤独》 - Marquez
《红楼梦》 - 曹雪芹
=== 组合搜索:科技类 + python ===
《Python编程》 - Eric
《Python进阶》 - Alice
3. 设计亮点
分层架构: 数据模型(Book)、业务逻辑(Library)、展示层(main)分离,各层职责清晰。
数据持久化: 用 CSV 文件保存,程序启动时自动加载——关闭程序数据不丢失。
借阅逻辑: 自动计算借阅天数,超 30 天提示逾期。
@dataclass 简化: Book 类用 @dataclass 装饰器,省去大量样板代码。
4. 扩展方向
| 功能 | 实现思路 |
|---|---|
| 用户系统 | 增加 User 类,记录借阅历史,限制最大借阅数量 |
| 逾期罚款 | 在 return_book 中计算罚款金额 |
| 图形界面 | 用 tkinter 或 Web 框架做界面 |
| 数据库存储 | 把 CSV 换成 SQLite 数据库 |
| 搜索增强 | 支持多条件联合搜索(作者+年份范围) |
❓ 常见问题
Q
@dataclass 和手写 __init__ 有什么区别?A
@dataclass 自动生成 __init__、__repr__、__eq__ 等方法,省去手写样板代码。功能完全一样,只是写法更简洁。如果类只需要存数据、不需要复杂逻辑,优先用 @dataclass。Q 为什么用 CSV 而不是 JSON 存数据?
A 图书数据是"表格型"的(每本书一行,字段固定),CSV 更适合这种结构,Excel 也能直接打开。JSON 更适合嵌套结构(比如学生成绩有子字典)。两种都能用,选哪种取决于数据结构。
Q 借阅状态只靠字符串标记"借出"/"在库"会不会出问题?
A 对于小项目够用了。更严谨的做法是用枚举
Enum 定义状态,避免手写字符串拼错。不过枚举要到更进阶才学,这里用字符串没问题。📖 小节
- 图书管理系统使用三层架构:数据模型 → 业务逻辑 → 展示层
@dataclass简化数据类的定义,to_csv_row()/from_csv_row()实现序列化- CSV 文件实现数据持久化,
load_data()启动时读取,save_data()每次修改后保存 datetime模块计算借阅天数,自动判断是否逾期- 列表推导式和字典
get()方法用于统计功能
📝 作业
-
基础题(难度⭐):给
Book类增加一个category字段(分类:文学/科技/历史等),并在添加图书时允许用户选择分类。 -
进阶题(难度⭐⭐):增加一个"借阅历史"功能——每次还书时,把借阅记录追加到
history.csv文件。记录格式:书名、借书人、借书日期、还书日期、是否逾期。 -
挑战题(难度⭐⭐⭐):不参考上面的代码,自己从零写一个"会员管理系统"。功能包括:添加会员(姓名、电话、积分)、消费积分(积分兑换)、查看会员列表、按积分排序、数据保存到 CSV。用类组织代码。