【初心者向け】Pythonのクラスを完全理解!定義から継承・実用例まで

python

Pythonで一歩進んだコードを書こうとすると、必ず出てくるのが「クラス」の概念です。

「クラスって難しそう…」「関数だけじゃダメなの?」「いつ使うべきなの?」と感じている方も多いでしょう。

実は、クラスを使うことで、コードの再利用性が高まり、複雑な処理も整理して扱えるようになります。

大きなプログラムを作るときには欠かせない技術です。

この記事では、Pythonのクラスについて基礎から応用まで解説します。

スポンサーリンク

クラスとは?オブジェクト指向の基礎知識

クラスの基本概念

クラスとは: 「オブジェクト(物体)を生み出す設計図」

例え話

  • クラス = たい焼きの型
  • オブジェクト(インスタンス) = 実際に作られたたい焼き
  • 属性(変数) = あんこの種類、大きさ
  • メソッド(関数) = 食べる、温める

なぜクラスが必要なの?

従来の関数中心の考え方

# 関数で犬の情報を管理
def bark_dog(name):
    print(f"{name}がワンワン!")

def feed_dog(name, food):
    print(f"{name}に{food}をあげた")

# 使用例
bark_dog("ポチ")
feed_dog("ポチ", "ドッグフード")

問題点

  • データ(犬の名前)と処理(吠える、食べる)がバラバラ
  • 犬の情報が増えると管理が大変
  • 複数の犬を扱うときにコードが複雑になる

クラスを使った改善

class Dog:
    def __init__(self, name):
        self.name = name
    
    def bark(self):
        print(f"{self.name}がワンワン!")
    
    def eat(self, food):
        print(f"{self.name}に{food}をあげた")

# 使用例
pochi = Dog("ポチ")
pochi.bark()
pochi.eat("ドッグフード")

メリット

  • データと処理がひとまとめになっている
  • 複数の犬を簡単に管理できる
  • コードの意味がわかりやすい

オブジェクト指向プログラミング(OOP)の基本

オブジェクト指向の3つの要素

  1. カプセル化:データと処理をひとまとめにする
  2. 継承:既存のクラスを元に新しいクラスを作る
  3. ポリモーフィズム:同じ操作でも対象によって動作が変わる

身近な例

  • スマートフォン:データ(連絡先、写真)と機能(電話、カメラ)が一体
  • :基本機能は共通だが、車種によって性能が異なる

基本的なクラスの作り方

最もシンプルなクラス

基本的な構文

class クラス名:
    def メソッド名(self):
        処理内容

実例

class Dog:
    def bark(self):
        print("ワン!")

# オブジェクトを作成(インスタンス化)
my_dog = Dog()

# メソッドを呼び出し
my_dog.bark()  # 出力: ワン!

重要なポイント

  • クラス名は大文字で始める(PascalCase)
  • selfは必ず第一引数に書く
  • selfは「自分自身」を指す特別な変数

selfの意味を理解しよう

selfとは: 「このオブジェクト自身」を指す特別な変数

例で理解

class Calculator:
    def add(self, a, b):
        result = a + b
        print(f"結果: {result}")
        return result

# 2つの電卓オブジェクトを作成
calc1 = Calculator()
calc2 = Calculator()

# それぞれ独立して動作
calc1.add(2, 3)  # 結果: 5
calc2.add(10, 20)  # 結果: 30

selfの働き

  • calc1.add(2, 3)を呼び出すとき
  • Pythonは自動的にCalculator.add(calc1, 2, 3)として実行
  • selfにはcalc1が入る

コンストラクタとインスタンス変数

__init__メソッド(コンストラクタ)

__init__とは: オブジェクトが作られたときに自動的に実行される特別なメソッド

基本的な使い方

class Person:
    def __init__(self, name, age):
        self.name = name  # インスタンス変数
        self.age = age    # インスタンス変数
    
    def introduce(self):
        print(f"こんにちは、{self.name}です。{self.age}歳です。")

# オブジェクトを作成(__init__が自動実行される)
alice = Person("アリス", 25)
bob = Person("ボブ", 30)

# メソッドを呼び出し
alice.introduce()  # こんにちは、アリスです。25歳です。
bob.introduce()    # こんにちは、ボブです。30歳です。

インスタンス変数の詳細

インスタンス変数の特徴

  • self.変数名で定義
  • オブジェクトごとに独立した値を持つ
  • オブジェクトが存在する限り値を保持

実例で確認

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner      # 口座名義
        self.balance = balance  # 残高
    
    def deposit(self, amount):
        self.balance += amount
        print(f"{amount}円を入金しました。残高: {self.balance}円")
    
    def withdraw(self, amount):
        if self.balance >= amount:
            self.balance -= amount
            print(f"{amount}円を出金しました。残高: {self.balance}円")
        else:
            print("残高不足です")
    
    def show_balance(self):
        print(f"{self.owner}さんの残高: {self.balance}円")

# 2つの口座を作成
account1 = BankAccount("田中", 10000)
account2 = BankAccount("佐藤", 5000)

# それぞれ独立して動作
account1.deposit(2000)   # 2000円を入金しました。残高: 12000円
account2.withdraw(1000)  # 1000円を出金しました。残高: 4000円

account1.show_balance()  # 田中さんの残高: 12000円
account2.show_balance()  # 佐藤さんの残高: 4000円

デフォルト引数の活用

便利な初期設定

class Student:
    def __init__(self, name, grade=1, subjects=None):
        self.name = name
        self.grade = grade
        self.subjects = subjects if subjects is not None else []
    
    def add_subject(self, subject):
        self.subjects.append(subject)
    
    def show_info(self):
        print(f"生徒名: {self.name}")
        print(f"学年: {self.grade}年生")
        print(f"履修科目: {self.subjects}")

# さまざまな作り方
student1 = Student("山田")  # デフォルト値を使用
student2 = Student("鈴木", 3)  # 学年を指定
student3 = Student("高橋", 2, ["数学", "英語"])  # すべて指定

student1.add_subject("国語")
student1.show_info()

クラス変数とインスタンス変数の違い

2つの変数の種類

インスタンス変数

  • self.変数名で定義
  • オブジェクトごとに異なる値を持つ

クラス変数

  • クラス直下で定義
  • すべてのオブジェクトで共有される

実例で比較

class Car:
    # クラス変数(すべての車で共通)
    wheels = 4
    fuel_type = "ガソリン"
    
    def __init__(self, maker, model, color):
        # インスタンス変数(車ごとに異なる)
        self.maker = maker
        self.model = model
        self.color = color
        self.mileage = 0
    
    def drive(self, distance):
        self.mileage += distance
        print(f"{distance}km走行しました。総走行距離: {self.mileage}km")
    
    def show_info(self):
        print(f"メーカー: {self.maker}")
        print(f"車種: {self.model}")
        print(f"色: {self.color}")
        print(f"タイヤ数: {self.wheels}")  # クラス変数
        print(f"走行距離: {self.mileage}km")

# 2台の車を作成
car1 = Car("トヨタ", "プリウス", "白")
car2 = Car("ホンダ", "フィット", "青")

# それぞれ別々に走行
car1.drive(100)
car2.drive(50)

car1.show_info()
print("---")
car2.show_info()

# クラス変数は共通
print(f"すべての車のタイヤ数: {Car.wheels}")

クラス変数の変更とその影響

クラス変数を変更

class Counter:
    count = 0  # クラス変数
    
    def __init__(self, name):
        self.name = name
        Counter.count += 1  # クラス変数を更新
    
    def show_count(self):
        print(f"{self.name}: 現在{Counter.count}個のオブジェクトが作られています")

# オブジェクトを作るたびにカウントが増える
obj1 = Counter("オブジェクト1")
obj1.show_count()  # オブジェクト1: 現在1個のオブジェクトが作られています

obj2 = Counter("オブジェクト2")
obj2.show_count()  # オブジェクト2: 現在2個のオブジェクトが作られています

obj3 = Counter("オブジェクト3")
obj1.show_count()  # オブジェクト1: 現在3個のオブジェクトが作られています

継承とオーバーライド

継承の基本概念

継承とは: 既存のクラスをベースに新しいクラスを作る仕組み

メリット

  • コードの再利用ができる
  • 共通機能をまとめて管理
  • 段階的な機能拡張が可能

基本的な書き方

class 親クラス名:
    # 親クラスの内容

class 子クラス名(親クラス名):
    # 子クラス独自の内容

継承の実例

基本的な継承

class Animal:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        print(f"{self.name}が何かの声を出した")
    
    def move(self):
        print(f"{self.name}が移動した")

class Dog(Animal):  # Animalクラスを継承
    def speak(self):  # 親クラスのメソッドをオーバーライド
        print(f"{self.name}がワンワン!")
    
    def fetch(self):  # 子クラス独自のメソッド
        print(f"{self.name}がボールを取ってきた")

class Cat(Animal):  # Animalクラスを継承
    def speak(self):  # 親クラスのメソッドをオーバーライド
        print(f"{self.name}がニャーニャー!")
    
    def climb(self):  # 子クラス独自のメソッド
        print(f"{self.name}が木に登った")

# 使用例
dog = Dog("ポチ")
cat = Cat("タマ")

# 共通のメソッド(継承されたもの)
dog.move()    # ポチが移動した
cat.move()    # タマが移動した

# オーバーライドされたメソッド
dog.speak()   # ポチがワンワン!
cat.speak()   # タマがニャーニャー!

# 子クラス独自のメソッド
dog.fetch()   # ポチがボールを取ってきた
cat.climb()   # タマが木に登った

super()を使った親クラスの活用

親クラスのメソッドを活用

class Vehicle:
    def __init__(self, maker, model):
        self.maker = maker
        self.model = model
        self.fuel = 100
    
    def start(self):
        print(f"{self.maker} {self.model}のエンジンをかけた")
    
    def stop(self):
        print(f"{self.maker} {self.model}のエンジンを止めた")

class Car(Vehicle):
    def __init__(self, maker, model, doors):
        super().__init__(maker, model)  # 親クラスの__init__を呼び出し
        self.doors = doors
    
    def start(self):
        super().start()  # 親クラスのstartメソッドを呼び出し
        print("ドアをロックしました")
    
    def show_info(self):
        print(f"車種: {self.maker} {self.model}")
        print(f"ドア数: {self.doors}")
        print(f"燃料残量: {self.fuel}%")

class Motorcycle(Vehicle):
    def __init__(self, maker, model, cc):
        super().__init__(maker, model)
        self.cc = cc
    
    def start(self):
        super().start()
        print("ヘルメットを着用してください")

# 使用例
car = Car("トヨタ", "カローラ", 4)
bike = Motorcycle("ホンダ", "CB750", 750)

car.start()
# 出力:
# トヨタ カローラのエンジンをかけた
# ドアをロックしました

bike.start()
# 出力:
# ホンダ CB750のエンジンをかけた
# ヘルメットを着用してください

実用例:クラスを使ったアプリケーション開発

家計簿アプリの作成

段階的に機能を作っていきます

基本的な支出クラス

class Expense:
    def __init__(self, date, category, amount, description=""):
        self.date = date
        self.category = category
        self.amount = amount
        self.description = description
    
    def show(self):
        desc = f" ({self.description})" if self.description else ""
        print(f"{self.date}: {self.category} - {self.amount}円{desc}")

# 使用例
expense1 = Expense("2024-01-15", "食費", 1500, "昼食")
expense2 = Expense("2024-01-15", "交通費", 300)

expense1.show()  # 2024-01-15: 食費 - 1500円 (昼食)
expense2.show()  # 2024-01-15: 交通費 - 300円

家計簿管理クラス

class HouseholdAccount:
    def __init__(self):
        self.expenses = []
    
    def add_expense(self, date, category, amount, description=""):
        expense = Expense(date, category, amount, description)
        self.expenses.append(expense)
        print(f"支出を追加しました: {category} {amount}円")
    
    def show_all_expenses(self):
        print("=== 支出一覧 ===")
        for expense in self.expenses:
            expense.show()
    
    def get_total_by_category(self, category):
        total = sum(expense.amount for expense in self.expenses 
                   if expense.category == category)
        return total
    
    def get_monthly_total(self, year_month):
        total = sum(expense.amount for expense in self.expenses 
                   if expense.date.startswith(year_month))
        return total
    
    def show_category_summary(self):
        categories = {}
        for expense in self.expenses:
            if expense.category in categories:
                categories[expense.category] += expense.amount
            else:
                categories[expense.category] = expense.amount
        
        print("=== カテゴリ別集計 ===")
        for category, total in categories.items():
            print(f"{category}: {total}円")

# 実際に使ってみる
account = HouseholdAccount()

# 支出を追加
account.add_expense("2024-01-15", "食費", 1500, "昼食")
account.add_expense("2024-01-15", "交通費", 300, "電車代")
account.add_expense("2024-01-16", "食費", 2000, "夕食")
account.add_expense("2024-01-16", "娯楽", 1200, "映画")
account.add_expense("2024-01-17", "食費", 800, "朝食")

# 一覧表示
account.show_all_expenses()
print()

# カテゴリ別集計
account.show_category_summary()
print()

# 特定カテゴリの合計
food_total = account.get_total_by_category("食費")
print(f"食費の合計: {food_total}円")

# 月別合計
january_total = account.get_monthly_total("2024-01")
print(f"1月の合計: {january_total}円")

ゲーム開発での応用

RPGゲームのキャラクター管理

class Character:
    def __init__(self, name, hp, attack, defense):
        self.name = name
        self.max_hp = hp
        self.hp = hp
        self.attack = attack
        self.defense = defense
        self.level = 1
        self.exp = 0
    
    def take_damage(self, damage):
        actual_damage = max(1, damage - self.defense)
        self.hp = max(0, self.hp - actual_damage)
        print(f"{self.name}は{actual_damage}のダメージを受けた!")
        
        if self.hp == 0:
            print(f"{self.name}は倒れた...")
            return True
        else:
            print(f"{self.name}の残りHP: {self.hp}/{self.max_hp}")
            return False
    
    def heal(self, amount):
        old_hp = self.hp
        self.hp = min(self.max_hp, self.hp + amount)
        healed = self.hp - old_hp
        print(f"{self.name}は{healed}回復した! HP: {self.hp}/{self.max_hp}")
    
    def attack_target(self, target):
        print(f"{self.name}は{target.name}を攻撃!")
        return target.take_damage(self.attack)

class Player(Character):
    def __init__(self, name):
        super().__init__(name, hp=100, attack=20, defense=5)
        self.items = []
    
    def use_item(self, item_name):
        if item_name in self.items:
            self.items.remove(item_name)
            if item_name == "回復薬":
                self.heal(30)
                print("回復薬を使用した!")
            return True
        else:
            print(f"{item_name}を持っていません")
            return False
    
    def add_item(self, item_name):
        self.items.append(item_name)
        print(f"{item_name}を手に入れた!")

class Enemy(Character):
    def __init__(self, name, hp, attack, defense, exp_reward):
        super().__init__(name, hp, attack, defense)
        self.exp_reward = exp_reward

# ゲームの実行例
player = Player("勇者")
enemy = Enemy("スライム", 50, 15, 2, 10)

player.add_item("回復薬")

print("=== バトル開始 ===")
while player.hp > 0 and enemy.hp > 0:
    # プレイヤーの攻撃
    enemy_defeated = player.attack_target(enemy)
    if enemy_defeated:
        print(f"{enemy.name}を倒した!")
        break
    
    # 敵の攻撃
    player_defeated = enemy.attack_target(player)
    if player_defeated:
        print("ゲームオーバー...")
        break
    
    # 回復アイテム使用
    if player.hp < 30 and "回復薬" in player.items:
        player.use_item("回復薬")

特殊メソッド(マジックメソッド)

よく使われる特殊メソッド

特殊メソッドとは: __で囲まれた、Pythonが特別に扱うメソッド

主要な特殊メソッド

class Book:
    def __init__(self, title, author, pages):
        self.title = title
        self.author = author
        self.pages = pages
    
    def __str__(self):
        # print()で呼ばれる
        return f"『{self.title}』 著者: {self.author}"
    
    def __repr__(self):
        # デバッグ用の表現
        return f"Book('{self.title}', '{self.author}', {self.pages})"
    
    def __len__(self):
        # len()関数で呼ばれる
        return self.pages
    
    def __eq__(self, other):
        # == で比較されたときに呼ばれる
        if isinstance(other, Book):
            return (self.title == other.title and 
                   self.author == other.author)
        return False
    
    def __lt__(self, other):
        # < で比較されたときに呼ばれる
        if isinstance(other, Book):
            return self.pages < other.pages
        return NotImplemented

# 使用例
book1 = Book("Python入門", "山田太郎", 300)
book2 = Book("Web開発", "佐藤花子", 450)
book3 = Book("Python入門", "山田太郎", 300)

print(book1)        # 『Python入門』 著者: 山田太郎
print(repr(book1))  # Book('Python入門', '山田太郎', 300)
print(len(book1))   # 300

print(book1 == book3)  # True
print(book1 == book2)  # False
print(book1 < book2)   # True (ページ数で比較)

# リストのソートも可能
books = [book2, book1]
books.sort()  # ページ数でソート
for book in books:
    print(book)

プロパティとアクセス制御

プロパティの使い方

@propertyデコレータ

class Temperature:
    def __init__(self, celsius=0):
        self._celsius = celsius
    
    @property
    def celsius(self):
        return self._celsius
    
    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("絶対零度より低い温度は設定できません")
        self._celsius = value
    
    @property
    def fahrenheit(self):
        return self._celsius * 9/5 + 32
    
    @fahrenheit.setter
    def fahrenheit(self, value):
        self.celsius = (value - 32) * 5/9
    
    @property
    def kelvin(self):
        return self._celsius + 273.15

# 使用例
temp = Temperature(25)

print(f"摂氏: {temp.celsius}°C")      # 25.0°C
print(f"華氏: {temp.fahrenheit}°F")   # 77.0°F
print(f"ケルビン: {temp.kelvin}K")    # 298.15K

# 華氏で設定
temp.fahrenheit = 86
print(f"摂氏: {temp.celsius}°C")      # 30.0°C

# エラーチェック
try:
    temp.celsius = -300
except ValueError as e:
    print(f"エラー: {e}")

プライベート属性

Pythonでの慣習

class BankAccount:
    def __init__(self, owner, initial_balance=0):
        self.owner = owner
        self._balance = initial_balance  # プライベート(慣習)
        self.__account_number = self._generate_account_number()  # より強いプライベート
    
    def _generate_account_number(self):
        # プライベートメソッド
        import random
        return f"{random.randint(1000000, 9999999)}"
    
    @property
    def balance(self):
        return self._balance
    
    def deposit(self, amount):
        if amount > 0:
            self._balance += amount
            return True
        return False
    
    def withdraw(self, amount):
        if 0 < amount <= self._balance:
            self._balance -= amount
            return True
        return False
    
    def get_account_info(self):
        return f"口座名義: {self.owner}, 口座番号: {self.__account_number}"

# 使用例
account = BankAccount("田中太郎", 10000)

print(account.balance)     # 10000 (プロパティ経由)
print(account.get_account_info())

# プライベート属性への直接アクセス(推奨されない)
print(account._balance)    # 10000 (アクセス可能だが推奨されない)

# より強いプライベート属性(名前マングリング)
# print(account.__account_number)  # エラー
print(account._BankAccount__account_number)  # アクセス可能だが推奨されない

よくある間違いと対処法

よくある初心者の間違い

間違い1:selfを忘れる

# ❌ 間違い
class Calculator:
    def add(a, b):  # selfがない
        return a + b

# ✅ 正解
class Calculator:
    def add(self, a, b):
        return a + b

間違い2:インスタンス変数とクラス変数の混同

# ❌ 間違い(共有されてしまう)
class Student:
    subjects = []  # クラス変数(すべてのインスタンスで共有)
    
    def __init__(self, name):
        self.name = name
    
    def add_subject(self, subject):
        self.subjects.append(subject)

# ✅ 正解
class Student:
    def __init__(self, name):
        self.name = name
        self.subjects = []  # インスタンス変数
    
    def add_subject(self, subject):
        self.subjects.append(subject)

間違い3:ミュータブルなデフォルト引数

# ❌ 間違い
class Classroom:
    def __init__(self, students=[]):  # 危険!
        self.students = students

# ✅ 正解
class Classroom:
    def __init__(self, students=None):
        self.students = students if students is not None else []

クラス設計のベストプラクティス

1. 単一責任の原則

# ❌ 悪い例:1つのクラスで複数の責任
class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email
    
    def save_to_database(self):
        # データベース操作
        pass
    
    def send_email(self):
        # メール送信
        pass
    
    def generate_report(self):
        # レポート生成
        pass

# ✅ 良い例:責任を分離
class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email

class UserRepository:
    def save(self, user):
        # データベース操作
        pass

class EmailService:
    def send_email(self, user, message):
        # メール送信
        pass

class ReportGenerator:
    def generate_user_report(self, user):
        # レポート生成
        pass

高度なクラス機能

クラスメソッドとスタティックメソッド

@classmethod と @staticmethod の使い分け

import datetime

class Person:
    population = 0  # クラス変数
    
    def __init__(self, name, birth_year):
        self.name = name
        self.birth_year = birth_year
        Person.population += 1
    
    def get_age(self):
        # インスタンスメソッド
        current_year = datetime.datetime.now().year
        return current_year - self.birth_year
    
    @classmethod
    def from_birth_date(cls, name, birth_date):
        # クラスメソッド:別のコンストラクタとして使用
        birth_year = birth_date.year
        return cls(name, birth_year)
    
    @classmethod
    def get_population(cls):
        # クラスメソッド:クラス変数へのアクセス
        return cls.population
    
    @staticmethod
    def is_adult(age):
        # スタティックメソッド:ユーティリティ関数
        return age >= 18
    
    @staticmethod
    def calculate_age_from_year(birth_year):
        # スタティックメソッド:年からの年齢計算
        current_year = datetime.datetime.now().year
        return current_year - birth_year

# 使用例
# 通常のインスタンス作成
person1 = Person("田中", 1990)

# クラスメソッドを使ったインスタンス作成
birth_date = datetime.date(1985, 5, 15)
person2 = Person.from_birth_date("佐藤", birth_date)

# インスタンスメソッド
print(f"{person1.name}の年齢: {person1.get_age()}歳")

# クラスメソッド
print(f"現在の人口: {Person.get_population()}人")

# スタティックメソッド
age = Person.calculate_age_from_year(2000)
print(f"2000年生まれの人の年齢: {age}歳")
print(f"成人かどうか: {Person.is_adult(age)}")

抽象クラスと抽象メソッド

ABCモジュールの使用

from abc import ABC, abstractmethod

class Shape(ABC):
    def __init__(self, name):
        self.name = name
    
    @abstractmethod
    def calculate_area(self):
        """面積を計算する(サブクラスで実装必須)"""
        pass
    
    @abstractmethod
    def calculate_perimeter(self):
        """周囲の長さを計算する(サブクラスで実装必須)"""
        pass
    
    def display_info(self):
        """共通メソッド"""
        print(f"図形: {self.name}")
        print(f"面積: {self.calculate_area()}")
        print(f"周囲の長さ: {self.calculate_perimeter()}")

class Rectangle(Shape):
    def __init__(self, width, height):
        super().__init__("長方形")
        self.width = width
        self.height = height
    
    def calculate_area(self):
        return self.width * self.height
    
    def calculate_perimeter(self):
        return 2 * (self.width + self.height)

class Circle(Shape):
    def __init__(self, radius):
        super().__init__("円")
        self.radius = radius
    
    def calculate_area(self):
        import math
        return math.pi * self.radius ** 2
    
    def calculate_perimeter(self):
        import math
        return 2 * math.pi * self.radius

# 使用例
rectangle = Rectangle(5, 3)
circle = Circle(4)

rectangle.display_info()
print()
circle.display_info()

# 抽象クラスは直接インスタンス化できない
# shape = Shape("図形")  # TypeError が発生

実際のプロジェクトでの活用例

Webアプリケーションでのモデル設計

簡単なブログシステム

from datetime import datetime
from typing import List, Optional

class User:
    def __init__(self, username: str, email: str, password_hash: str):
        self.id: Optional[int] = None
        self.username = username
        self.email = email
        self.password_hash = password_hash
        self.created_at = datetime.now()
        self.posts: List['BlogPost'] = []
    
    def create_post(self, title: str, content: str) -> 'BlogPost':
        post = BlogPost(title, content, self)
        self.posts.append(post)
        return post
    
    def get_post_count(self) -> int:
        return len(self.posts)
    
    def __str__(self):
        return f"User({self.username})"

class BlogPost:
    def __init__(self, title: str, content: str, author: User):
        self.id: Optional[int] = None
        self.title = title
        self.content = content
        self.author = author
        self.created_at = datetime.now()
        self.updated_at = datetime.now()
        self.comments: List['Comment'] = []
        self.tags: List[str] = []
    
    def update_content(self, new_title: str = None, new_content: str = None):
        if new_title:
            self.title = new_title
        if new_content:
            self.content = new_content
        self.updated_at = datetime.now()
    
    def add_comment(self, content: str, author: User) -> 'Comment':
        comment = Comment(content, author, self)
        self.comments.append(comment)
        return comment
    
    def add_tag(self, tag: str):
        if tag not in self.tags:
            self.tags.append(tag)
    
    def get_summary(self, length: int = 100) -> str:
        if len(self.content) <= length:
            return self.content
        return self.content[:length] + "..."
    
    def __str__(self):
        return f"BlogPost('{self.title}' by {self.author.username})"

class Comment:
    def __init__(self, content: str, author: User, post: BlogPost):
        self.id: Optional[int] = None
        self.content = content
        self.author = author
        self.post = post
        self.created_at = datetime.now()
    
    def __str__(self):
        return f"Comment by {self.author.username} on {self.post.title}"

class Blog:
    def __init__(self, title: str):
        self.title = title
        self.users: List[User] = []
        self.posts: List[BlogPost] = []
    
    def register_user(self, username: str, email: str, password_hash: str) -> User:
        user = User(username, email, password_hash)
        self.users.append(user)
        return user
    
    def publish_post(self, post: BlogPost):
        if post not in self.posts:
            self.posts.append(post)
    
    def get_recent_posts(self, limit: int = 10) -> List[BlogPost]:
        sorted_posts = sorted(self.posts, key=lambda p: p.created_at, reverse=True)
        return sorted_posts[:limit]
    
    def search_posts(self, keyword: str) -> List[BlogPost]:
        results = []
        for post in self.posts:
            if (keyword.lower() in post.title.lower() or 
                keyword.lower() in post.content.lower() or
                keyword.lower() in post.tags):
                results.append(post)
        return results

# 使用例
blog = Blog("私のブログ")

# ユーザー登録
user1 = blog.register_user("alice", "alice@example.com", "hashed_password_123")
user2 = blog.register_user("bob", "bob@example.com", "hashed_password_456")

# 記事投稿
post1 = user1.create_post("Pythonクラスの基本", "Pythonのクラスについて学びました...")
post1.add_tag("Python")
post1.add_tag("プログラミング")
blog.publish_post(post1)

post2 = user2.create_post("Web開発入門", "HTMLとCSSを使ってWebページを作りました...")
post2.add_tag("Web開発")
post2.add_tag("HTML")
blog.publish_post(post2)

# コメント投稿
comment1 = post1.add_comment("とても参考になりました!", user2)

# ブログの情報表示
print(f"ブログタイトル: {blog.title}")
print(f"登録ユーザー数: {len(blog.users)}")
print(f"投稿数: {len(blog.posts)}")

print("\n最新の投稿:")
for post in blog.get_recent_posts(5):
    print(f"- {post.title} by {post.author.username}")
    print(f"  {post.get_summary(50)}")
    print(f"  タグ: {', '.join(post.tags)}")
    print(f"  コメント数: {len(post.comments)}")
    print()

# 検索機能
search_results = blog.search_posts("Python")
print(f"'Python'で検索した結果: {len(search_results)}件")
for post in search_results:
    print(f"- {post.title}")

デバッグとテストのためのクラス設計

テストしやすいクラスの作り方

依存関係の注入

class EmailSender:
    def send(self, to: str, subject: str, body: str):
        # 実際のメール送信処理
        print(f"メール送信: {to} - {subject}")

class MockEmailSender:
    def __init__(self):
        self.sent_emails = []
    
    def send(self, to: str, subject: str, body: str):
        # テスト用のモック
        self.sent_emails.append({
            'to': to,
            'subject': subject,
            'body': body
        })

class UserService:
    def __init__(self, email_sender):
        self.email_sender = email_sender
    
    def register_user(self, username: str, email: str):
        # ユーザー登録処理
        user = User(username, email, "dummy_hash")
        
        # ウェルカムメール送信
        self.email_sender.send(
            email,
            "登録完了",
            f"{username}さん、登録ありがとうございます!"
        )
        
        return user

# 本番環境
production_service = UserService(EmailSender())
production_service.register_user("alice", "alice@example.com")

# テスト環境
mock_sender = MockEmailSender()
test_service = UserService(mock_sender)
test_service.register_user("test_user", "test@example.com")

# テスト結果確認
print(f"送信されたメール数: {len(mock_sender.sent_emails)}")
print(f"最初のメール: {mock_sender.sent_emails[0]}")

まとめ

Pythonのクラスは、プログラムを「現実の物事のように」モデル化する強力な手段です。

今日学んだ重要ポイント

クラスの基本

  • classでクラス定義、initで初期化
  • selfは「自分自身」を指す特別な変数
  • メソッドでオブジェクトの動作を定義

重要な概念

  • インスタンス変数:オブジェクトごとに独立した値
  • クラス変数:すべてのオブジェクトで共有される値
  • 継承:既存クラスを元に新しいクラスを作成
  • オーバーライド:親クラスのメソッドを子クラスで再定義

実用的なテクニック

  • 特殊メソッド__str____len__など
  • プロパティ@propertyデコレータ
  • クラスメソッド@classmethod
  • スタティックメソッド@staticmethod

設計のポイント

  • 単一責任の原則:1つのクラスは1つの責任
  • カプセル化:データと処理をまとめる
  • 継承の適切な使用:is-a関係でのみ使用
  • テストしやすい設計:依存関係の注入

よくある間違い

  • selfを忘れる
  • クラス変数とインスタンス変数の混同
  • ミュータブルなデフォルト引数

コメント

タイトルとURLをコピーしました