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 定义状态,避免手写字符串拼错。不过枚举要到更进阶才学,这里用字符串没问题。

📖 小节


📝 作业

  1. 基础题(难度⭐):给 Book 类增加一个 category 字段(分类:文学/科技/历史等),并在添加图书时允许用户选择分类。

  2. 进阶题(难度⭐⭐):增加一个"借阅历史"功能——每次还书时,把借阅记录追加到 history.csv 文件。记录格式:书名、借书人、借书日期、还书日期、是否逾期。

  3. 挑战题(难度⭐⭐⭐):不参考上面的代码,自己从零写一个"会员管理系统"。功能包括:添加会员(姓名、电话、积分)、消费积分(积分兑换)、查看会员列表、按积分排序、数据保存到 CSV。用类组织代码。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏