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
alice と bob は同じ User クラスから作られていますが、name や login_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. 最初の作り方
いきなりクラスから書くのではなく、まず「何をまとめたいのか」を決めると失敗しにくいです。
- 同じデータのまとまりが複数出てくるか確認する
- そのデータに対して行う処理を書き出す
- データを属性、処理をメソッドとしてクラスに入れる
- 外から直接変更してほしくない値は、メソッド経由で変更する
- 似たクラスが増えてから、継承や共通化を考える
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 | データ中心のクラスを短く書く仕組み |