プロジェクト:図書館管理システム

これはフェーズ 4 の最終プロジェクトです。ここ数回のレッスンのすべて——クラス、継承、カプセル化、マジックメソッド、ファイル操作——を結集して、実際に使える図書館管理システムを構築します。このプロジェクトを完成させることで、Python のオブジェクト指向プログラミングを真に習得したことになります。


プロジェクト要件

TEXT
1. 書籍管理(CRUD)
   ├── 書籍追加(タイトル、著者、ISBN、出版年)
   ├── 書籍削除(ISBN で)
   ├── 書籍情報修正
   └── 書籍検索(タイトルの曖昧検索/著者検索)

2. 貸出管理
   ├── 貸出(借り手と貸出日を記録)
   ├── 返却(延滞チェック)
   └── 貸出記録の表示

3. データ統計
   ├── 総書籍数
   ├── 利用可能/貸出中の数
   └── 著者別の数

4. データ永続化(CSV ファイル、起動時に自動読み込み)

1. 完全なコード

例:図書館管理システム

PYTHON
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"Loaded {len(self.books)} books.")

    def save_data(self):
        """CSV にデータを保存"""
        with open(self.DATA_FILE, "w", newline="", encoding="utf-8") as f:
            writer = csv.writer(f)
            writer.writerow(["Title", "Author", "ISBN", "Year",
                            "Borrowed", "Borrower", "Borrow Date"])
            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 already exists!"
        self.books.append(Book(title, author, isbn, year))
        self.save_data()
        return True, f"Added '{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"Deleted '{removed.title}'"
        return False, "Book with this ISBN not found."

    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, "Book not found."
        if book.is_borrowed:
            return False, f"'{book.title}' is already borrowed by {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}' has been borrowed by {borrower}."

    def return_book(self, isbn):
        """書籍を返却"""
        book = self.get_book_by_isbn(isbn)
        if not book:
            return False, "Book not found."
        if not book.is_borrowed:
            return False, f"'{book.title}' is not currently borrowed."
        # 延滞チェック(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}' returned. Overdue by {days_borrowed - 30} days."
        return True, f"'{book.title}' returned. Borrowed for {days_borrowed} days."

    # ---- 統計 ----

    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("Library Management System")
    print("=" * 40)
    print("1. Add Book")
    print("2. Delete Book")
    print("3. Search Books")
    print("4. Display All Books")
    print("5. Borrow Book")
    print("6. Return Book")
    print("7. Statistics")
    print("8. Exit")
    print("=" * 40)


def display_books(books, title="Search Results"):
    """書籍リストを表示"""
    if not books:
        print(f"\n{title}: No records.")
        return
    print(f"\n--- {title} ---")
    print(f"{'Title':<20}{'Author':<12}{'ISBN':<15}{'Status':<8}")
    print("-" * 55)
    for b in books:
        status = "Borrowed" if b.is_borrowed else "Available"
        print(f"{b.title:<20}{b.author:<12}{b.isbn:<15}{status:<8}")


def main():
    library = Library()

    while True:
        show_menu()
        choice = input("Select option (1-8): ").strip()

        if choice == "1":
            title = input("Title: ").strip()
            author = input("Author: ").strip()
            isbn = input("ISBN: ").strip()
            year = input("Year: ").strip()
            success, msg = library.add_book(title, author, isbn, int(year))
            print(f"  {'OK' if success else 'Error'}: {msg}")

        elif choice == "2":
            isbn = input("Enter ISBN to delete: ").strip()
            success, msg = library.delete_book(isbn)
            print(f"  {'OK' if success else 'Error'}: {msg}")

        elif choice == "3":
            print("  1. Search by Title")
            print("  2. Search by Author")
            opt = input("Choose: ").strip()
            if opt == "1":
                keyword = input("Enter title keyword: ").strip()
                results = library.search_by_title(keyword)
            elif opt == "2":
                author = input("Enter author name: ").strip()
                results = library.search_by_author(author)
            else:
                print("Invalid choice")
                continue
            display_books(results)

        elif choice == "4":
            display_books(library.books, "All Books")

        elif choice == "5":
            isbn = input("Enter ISBN to borrow: ").strip()
            borrower = input("Borrower name: ").strip()
            success, msg = library.borrow_book(isbn, borrower)
            print(f"  {'OK' if success else 'Error'}: {msg}")

        elif choice == "6":
            isbn = input("Enter ISBN to return: ").strip()
            success, msg = library.return_book(isbn)
            print(f"  {'OK' if success else 'Error'}: {msg}")

        elif choice == "7":
            stats = library.get_stats()
            print(f"\n--- Statistics ---")
            print(f"Total books: {stats['total']}")
            print(f"Available: {stats['available']}  Borrowed: {stats['borrowed']}")
            print(f"\nBy Author:")
            for author, count in stats['by_author'].items():
                print(f"  {author}: {count} books")

        elif choice == "8":
            print("Thank you for using! Goodbye!")
            break

        else:
            print("Invalid choice. Please enter 1-8.")


if __name__ == "__main__":
    main()
▶ 試してみよう

2. 設計のポイント

レイヤー構造: データモデル(Book)、ビジネスロジック(Library)、プレゼンテーション層(main)が分離され、各層の責任が明確です。

データ永続化: CSV ファイル保存、起動時に自動読み込み——プログラム再起動後もデータは保持されます。

貸出ロジック: 貸出期間を自動計算。30 日を超えると延滞警告を表示。

@dataclass による簡略化: Book クラスは @dataclass デコレータを使用し、多くのボイラープレートコードを削減。


❓ よくある質問

Q @dataclass と手動で __init__ を書くことの違いは何ですか?
A @dataclass__init____repr____eq__ などのメソッドを自動生成し、ボイラープレートコードを書く手間を省きます。機能的には同じで、より簡潔です。クラスが主にデータを保存し、複雑なロジックがない場合は @dataclass を優先してください。
Q なぜ JSON ではなく CSV をデータ保存に使うのですか?
A 書籍データは「表形式」(1 行 1 冊、固定フィールド)です——CSV はこの構造に適しており、Excel で直接開けます。JSON はネスト構造(サブ辞書を持つ学生スコアなど)に適しています。両方とも使えます。データ構造に基づいて選択しましょう。
Q ステータスに「Borrowed」/「Available」のような文字列を使うと問題がありますか?
A 小規模プロジェクトでは十分です。より厳密なアプローチは Enum を使って状態を定義し、文字列のタイプミスを防ぐことです。ただし、列挙型は高度なトピックなので、ここでは文字列で十分です。

📖 まとめ


📝 練習問題

  1. 初級(難易度 ⭐)Book クラスに category フィールド(Fiction、Science、History など)を追加し、書籍追加時にカテゴリを選択できるようにしてください。

  2. 中級(難易度 ⭐⭐):「貸出履歴」機能を追加——書籍が返却されるたびに、貸出記録を history.csv ファイルに追記します。記録形式:書籍タイトル、借り手、貸出日、返却日、延滞状況。

  3. 上級(難易度 ⭐⭐⭐):上記のコードを参照せずに、ゼロから「会員管理システム」を構築してください。機能:会員追加(名前、電話番号、ポイント)、ポイント交換、会員リスト表示、ポイント順ソート、CSV へのデータ保存。クラスを使ってコードを整理してください。

100%