Pythonオブジェクト指向の基礎

class、インスタンス、self、継承、ポリモーフィズム、dataclassまでを、Python初学者向けに整理するオブジェクト指向入門ガイド。

Pythonのオブジェクト指向は、関連するデータと処理を「オブジェクト」というまとまりで扱うための考え方です。この記事では、クラス、インスタンス、self、メソッド、継承、ポリモーフィズムを、実際のコード例と一緒に整理します。

1. オブジェクト指向とは何か

オブジェクト指向は、プログラムを「データ」と「そのデータを扱う処理」のまとまりとして設計する考え方です。

たとえば、ユーザー情報を扱うコードでは、名前、メールアドレス、ログイン回数などのデータと、「ログイン回数を増やす」「表示名を作る」といった処理がセットで出てきます。これらを別々の変数と関数として散らばらせるのではなく、User というまとまりに閉じ込めると、コードの責任範囲が見えやすくなります。

flowchart LR
  C["class User"] --> D["データ: name, email, login_count"]
  C --> M["処理: login(), display_name()"]
  C --> O1["user1 インスタンス"]
  C --> O2["user2 インスタンス"]
    
最初の理解: クラスは「設計図」、インスタンスは「設計図から作った実物」です。クラスそのものよりも、「同じ種類のデータと処理をまとめたい場面で使う」と考えると入りやすいです。

2. 何のために使うのか

オブジェクト指向は、コード量を短くするためだけのものではありません。変更しやすい単位を作るための考え方です。

関係するものをまとめる

ユーザーのデータとユーザー操作、商品データと価格計算など、同じ責任を持つものを1か所に集められます。

状態を持たせる

関数だけでは扱いづらい「現在の値」「回数」「設定」などを、インスタンスごとに保持できます。

共通処理を再利用する

似た種類のオブジェクトに共通する処理を親クラスへまとめ、必要な部分だけ子クラスで変えられます。

3. クラスとインスタンスの関係

クラスは型を定義し、インスタンスはその型から作られた具体的な値です。

用語意味
クラス データと処理の設計図 class User:
インスタンス クラスから作られた実物 user = User("Alice")
属性 インスタンスが持つデータ user.name
メソッド クラス内に定義された関数 user.login()
self 呼び出し中のインスタンス自身 self.name
class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email
        self.login_count = 0

    def login(self):
        self.login_count += 1

    def display_name(self):
        return f"{self.name} <{self.email}>"


alice = User("Alice", "alice@example.com")
bob = User("Bob", "bob@example.com")

alice.login()
alice.login()
bob.login()

print(alice.display_name())  # Alice <alice@example.com>
print(alice.login_count)     # 2
print(bob.login_count)       # 1

alicebob は同じ User クラスから作られていますが、namelogin_count は別々に持っています。これがインスタンスを使う大きな理由です。

4. selfと__init__

__init__ はインスタンス作成時の初期設定を行うメソッドで、self は作成されたインスタンス自身を指します。

Pythonでは、メソッドの第1引数として self を明示的に書きます。呼び出す側では user.login() のように書きますが、メソッド内部では「どのユーザーのログイン回数を増やすのか」を self で表します。

class Counter:
    def __init__(self):
        self.value = 0

    def increment(self):
        self.value += 1


counter = Counter()
counter.increment()
counter.increment()

print(counter.value)  # 2
つまずきやすい点: self は特別な予約語ではありませんが、Pythonでは慣習として必ず self と書きます。別名にもできますが、読み手を混乱させるので避けます。

5. インスタンス属性とクラス属性

インスタンスごとに違う値はインスタンス属性、クラス全体で共有したい値はクラス属性として扱います。

class Product:
    tax_rate = 0.10  # すべての商品で共有するクラス属性

    def __init__(self, name, price):
        self.name = name      # 商品ごとに違うインスタンス属性
        self.price = price

    def price_with_tax(self):
        return int(self.price * (1 + Product.tax_rate))


book = Product("Python入門", 2000)
pen = Product("ペン", 150)

print(book.price_with_tax())  # 2200
print(pen.price_with_tax())   # 165
種類置き場所使う場面
インスタンス属性 self.name のように代入 ユーザー名、価格、状態など、個体ごとに違う値
クラス属性 クラス直下に定義 税率、上限値、種類名など、全体で共有する値

6. カプセル化

カプセル化は、データの変更ルールをメソッドの中にまとめ、外側から雑に状態を書き換えにくくする考え方です。

Pythonには、他の言語の private のような強いアクセス制限はありません。その代わり、_balance のように先頭へアンダースコアを付け、「外から直接触る前提ではない」という意図を示すことがよくあります。

class BankAccount:
    def __init__(self, owner, initial_balance=0):
        self.owner = owner
        self._balance = initial_balance

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("入金額は1以上にしてください")
        self._balance += amount

    def withdraw(self, amount):
        if amount <= 0:
            raise ValueError("出金額は1以上にしてください")
        if amount > self._balance:
            raise ValueError("残高が不足しています")
        self._balance -= amount

    def get_balance(self):
        return self._balance


account = BankAccount("田中", 1000)
account.deposit(500)
account.withdraw(300)

print(account.get_balance())  # 1200

外側から account._balance = -9999 のように書くこと自体はできます。しかし、入金と出金のルールをメソッドに集めておくと、コードを読む人に「この操作を通して変更してほしい」という意図を伝えられます。

7. 継承

継承は、既存のクラスを土台にして、新しいクラスを作る仕組みです。

共通する処理を親クラスに置き、違う部分だけ子クラスで追加・上書きできます。Pythonでは、子クラスのメソッドから親クラスのメソッドを呼ぶときに super() を使います。

class Notification:
    def __init__(self, message):
        self.message = message

    def format_message(self):
        return f"[通知] {self.message}"

    def send(self):
        raise NotImplementedError("送信方法は子クラスで定義してください")


class EmailNotification(Notification):
    def __init__(self, message, email):
        super().__init__(message)
        self.email = email

    def send(self):
        print(f"{self.email} にメール送信: {self.format_message()}")


class SlackNotification(Notification):
    def __init__(self, message, channel):
        super().__init__(message)
        self.channel = channel

    def send(self):
        print(f"{self.channel} に投稿: {self.format_message()}")


email = EmailNotification("デプロイが完了しました", "admin@example.com")
slack = SlackNotification("テストが成功しました", "#dev")

email.send()
slack.send()
使いすぎ注意: 継承は便利ですが、階層が深くなると処理の流れを追いにくくなります。単に機能を部品として持たせたいだけなら、別クラスを属性として持つ「合成」も検討します。

8. ポリモーフィズム

ポリモーフィズムは、違う種類のオブジェクトを同じ操作で扱える性質です。

たとえば、メール通知とSlack通知は中身の送信方法が違います。それでも両方に send() メソッドがあれば、呼び出す側は具体的な種類を気にせず notification.send() と書けます。

notifications = [
    EmailNotification("請求書を送信しました", "billing@example.com"),
    SlackNotification("本番反映が完了しました", "#release"),
]

for notification in notifications:
    notification.send()

Pythonでは「同じ親クラスを継承しているか」よりも、「必要なメソッドを持っているか」が重視される場面が多いです。この考え方は、アヒルのように歩き、アヒルのように鳴くならアヒルとして扱う、という意味でダックタイピングと呼ばれます。

9. dataclassでデータ中心のクラスを書く

データを持つことが主目的のクラスは、dataclass を使うと短く読みやすく書けます。

通常のクラスでは __init__ を自分で書きますが、@dataclass を使うと、属性定義から初期化メソッドなどを自動生成してくれます。

from dataclasses import dataclass


@dataclass
class Task:
    title: str
    done: bool = False

    def complete(self):
        self.done = True


task = Task("Pythonの記事を読む")
print(task)  # Task(title='Pythonの記事を読む', done=False)

task.complete()
print(task.done)  # True
書き方向いている場面
通常のクラス 初期化処理や振る舞いを細かく制御したい
dataclass 属性を持つデータ構造を簡潔に定義したい

10. 最初の作り方

いきなりクラスから書くのではなく、まず「何をまとめたいのか」を決めると失敗しにくいです。

  1. 同じデータのまとまりが複数出てくるか確認する
  2. そのデータに対して行う処理を書き出す
  3. データを属性、処理をメソッドとしてクラスに入れる
  4. 外から直接変更してほしくない値は、メソッド経由で変更する
  5. 似たクラスが増えてから、継承や共通化を考える
class TodoItem:
    def __init__(self, title):
        self.title = title
        self.done = False

    def complete(self):
        self.done = True

    def label(self):
        status = "完了" if self.done else "未完了"
        return f"[{status}] {self.title}"


todo = TodoItem("オブジェクト指向を復習する")
print(todo.label())  # [未完了] オブジェクト指向を復習する

todo.complete()
print(todo.label())  # [完了] オブジェクト指向を復習する

11. 関数だけで書く場合との違い

関数だけで十分な場面もあります。オブジェクト指向は、状態と処理を一緒に管理したいときに効果が出ます。

状況関数中心が向くクラスが向く
単発の変換処理 文字列整形、数値計算、ファイル名変換 あまり必要ない
状態を持つ処理 引数と戻り値が増えやすい カウンター、ユーザー、タスク、ゲームキャラクター
種類ごとに処理が変わる if 文が増えやすい 通知、決済方法、図形、ファイル出力形式
テストしやすさ 純粋な関数はテストしやすい 状態の変化をまとめて確認しやすい
判断基準: 「この処理は、何かの状態を覚え続ける必要があるか?」と考えると判断しやすいです。状態がないなら関数で十分なことも多いです。

12. 初心者がつまずきやすい点

Pythonのオブジェクト指向では、文法そのものよりも「何をクラスにするか」で迷いやすいです。

何でもクラスにしない

短い変換処理や一度だけ使う処理は、関数のままのほうが読みやすいことがあります。

self を忘れない

インスタンス属性やメソッドを使うには、メソッド定義の第1引数に self が必要です。

継承を深くしすぎない

親子関係が何段にもなると、どのメソッドが実行されるのか追いにくくなります。

可変のクラス属性に注意する

リストや辞書をクラス属性にすると、複数インスタンスで共有されて思わぬ変更につながることがあります。

class BadTodoList:
    items = []  # 全インスタンスで共有されてしまう

    def add(self, item):
        self.items.append(item)


class GoodTodoList:
    def __init__(self):
        self.items = []  # インスタンスごとに別のリストを持つ

    def add(self, item):
        self.items.append(item)

13. まとめ

Pythonのオブジェクト指向は、データと処理を意味のある単位にまとめ、状態を持つコードを読みやすくするための道具です。

まず覚えること意味
クラスデータと処理の設計図
インスタンスクラスから作った具体的なオブジェクト
__init__インスタンス作成時の初期化処理
self今操作しているインスタンス自身
継承既存クラスを土台に新しいクラスを作る仕組み
ポリモーフィズム違う種類のオブジェクトを同じ操作で扱う性質
dataclassデータ中心のクラスを短く書く仕組み
このページのゴール: オブジェクト指向を「難しい設計手法」として暗記するのではなく、「同じ責任を持つデータと処理をまとめる方法」として使えるようになることです。

参考ソース