順番に読み進めながら学べます

ポリモーフィズム — 同じ名前で型ごとに違う動きをさせる

Python のポリモーフィズムを解説します。親クラスのメソッドを子クラスでオーバーライドして、呼び出し側は型を意識せず使える設計、if type(...) の分岐を消すパターンまで図解で押さえます。

前回までで継承と MRO の仕組みを押さえました。仕上げとして、オブジェクト指向の三大要素のひとつ — ポリモーフィズム(polymorphism) を整理します。

ポリモーフィズムとは

ポリモーフィズム(polymorphism)は 「同じインターフェース(メソッド名)で、型ごとに違う動きをする」 という考え方です。たとえば「給与を計算する」という処理を、従業員(Employee)マネージャー(Manager)エンジニア(Engineer) のそれぞれで違う計算式にしたい、というケースを考えます。

親クラス Employeecalculate_salary メソッドを置いて、子クラス ManagerEngineer は同じ名前のメソッドを 自分用の計算式でオーバーライド します。こうしておくと、呼び出す側は 「どのクラスかを気にせず employee.calculate_salary() と書くだけ」 で、それぞれに合った計算が実行されます。

calculate_salary を 3 クラスで上書きする
Employee(親)base_salary(そのまま)= 30万Manager(子・上書き)base +team × 5万= 120万Engineer(子・上書き)base +skill × 2万= 38万
親クラス Employeecalculate_salary を定義 → 子クラス Manager / Engineer がそれぞれ自分用の式で 上書き。同じメソッド名でクラスごとに違う計算が走る。

親クラスを土台に、子クラスで計算式を変える

実例として給与計算を作ってみます。Employee を親にして、Manager(チームサイズで上乗せ)と Engineer(スキルレベルで上乗せ)を子クラスとして定義します。各クラスが 同じ名前 calculate_salary を持っているのがポイントです。

class Employee:
    def __init__(self, name, base_salary):
        self.name        = name
        self.base_salary = base_salary

    def calculate_salary(self):                  # 一般従業員 = 基本給だけ
        return self.base_salary


class Manager(Employee):
    def __init__(self, name, base_salary, team_size):
        super().__init__(name, base_salary)
        self.team_size = team_size

    def calculate_salary(self):                  # チームサイズに応じて加算
        return self.base_salary + self.team_size * 50000


class Engineer(Employee):
    def __init__(self, name, base_salary, skill_level):
        super().__init__(name, base_salary)
        self.skill_level = skill_level

    def calculate_salary(self):                  # スキルレベルに応じて加算
        return self.base_salary + self.skill_level * 20000

給与計算と同じパターンを、通知(Notification)のドメイン で試します。

class Notification: を定義し、__init__(self, recipient)self.recipient = recipient を代入してください。send(self) を定義し、return f"通知を {self.recipient} に送信" を返してください。

class EmailNotification(Notification): を定義し、__init__(self, recipient, subject)super().__init__(recipient) を呼んでから self.subject = subject を代入してください。send をオーバーライドし、return f"[Email] {self.recipient} に「{self.subject}」を送信" を返してください。

class SmsNotification(Notification): を定義し、追加属性は bodysend の戻り値は f"[SMS] {self.recipient} に「{self.body}」を送信" にしてください。

Notification("田中") / EmailNotification("sato@example.com", "確認事項") / SmsNotification("090-1234-5678", "本日 19 時集合") を作り、それぞれ send() の戻り値を print してください。

(正しく実行できれば解説が表示されます)

Python エディタ

コードを実行してください
for ループ内の emp.calculate_salary() の動き
emp.calculate_salary()(同じ 1 行)emp = tanaka(Employee)→ 30万emp = sato(Manager)→ 120万emp = suzuki(Engineer)→ 38万EmployeeManagerEngineer
ループの中身は emp.calculate_salary() の 1 行だけ。それなのに、emp がどのクラスかに応じて 自動で違うメソッドが選ばれる — これがポリモーフィズムの力。

リストでまとめて扱う — 「型を気にしないループ」

ポリモーフィズムが本領を発揮するのは、異なる型のオブジェクトを 1 つのリストに詰めてまとめて処理する ような場面です。給与計算システムを PayrollSystem クラスとしてまとめると、ループの中身がきれいに 1 行で済みます。

class PayrollSystem:
    def __init__(self):
        self.employees = []

    def add(self, employee):
        self.employees.append(employee)

    def total(self):
        result = 0
        for emp in self.employees:                       # 型はバラバラでよい
            result += emp.calculate_salary()              # 同じメソッド名で OK
        return result


payroll = PayrollSystem()
payroll.add(Employee("田中", 300000))
payroll.add(Manager("佐藤",  800000, 8))
payroll.add(Engineer("鈴木", 300000, 4))

print(payroll.total())   # 1880000
PayrollSystem は中身の型を区別しない
PayrollSystem
  • employees = [...](種類が混ざっている)
  • for emp in self.employees: で順に処理
  • emp.calculate_salary() を呼ぶだけ
Employee(一般)
  • base_salary を返す
Manager
  • base + team * 5万
Engineer
  • base + skill * 2万
リストの中に 3 種類のクラスが混ざっていても、PayrollSystem 側のコードは 「同じメソッド名で呼ぶ」 だけで型ごとに正しい計算が走る。

実践 1 で定義した 3 クラス をそのまま再利用して、複数の通知をリストにまとめて一括送信する NotificationCenter を書きます(コンソールは状態が残るので、前の演習のクラスはそのまま使えます)。

class NotificationCenter: を定義し、__init__self.queue = [] を初期化。add(self, notification)self.queue.append(notification) し、send_all(self)for n in self.queue: ループの中で print(n.send()) を実行してください。

center = NotificationCenter() を作り、add で 3 通の通知を追加してください(EmailNotification("sato@example.com", "確認事項") / SmsNotification("090-1234-5678", "本日 19 時集合") / EmailNotification("tanaka@example.com", "週次レポート"))。

center.send_all() を呼んで、3 通のメッセージが順に表示されることを確認してください。

Python エディタ

コードを実行してください

ポリモーフィズムを使わなかった場合の比較

もしポリモーフィズムを使わずに同じことを書こうとすると、if type(emp) == ...: で型を見て分岐する ような書き方になります。これは動きはするものの、新しい職種を追加するたびに if を 1 段増やす 必要があり、忘れた瞬間にバグが生まれます。

# ❌ ポリモーフィズムを使わない(型で分岐する)書き方
def calc(emp):
    if type(emp) is Manager:
        return emp.base_salary + emp.team_size * 50000
    elif type(emp) is Engineer:
        return emp.base_salary + emp.skill_level * 20000
    else:
        return emp.base_salary


# ✅ ポリモーフィズム版(クラス側に処理を寄せる)
def calc(emp):
    return emp.calculate_salary()         # 1 行で済む
型分岐 vs ポリモーフィズム
❌ 型分岐型if type ==× 数だけ新型のたび分岐を追加✅ ポリモルemp.calculate()新型はクラス追加のみ波及閉じる
型分岐型 は呼び出し側に if type が並び、新しい型が増えるたびに分岐を増やす必要がある。ポリモーフィズム型 は呼び出し側を 1 行に保ったまま、新しい型はクラスを足すだけで対応できる。

「呼び出し側は型を気にしない」が合言葉

ポリモーフィズムが効いている設計かどうかは、呼び出し側のコードに if type(...) や if isinstance(...) が並んでいないか で判断できます。並んでしまっているなら、その分岐をクラス側のメソッドオーバーライドに引っ越す のがリファクタリングの定石です。「クラスを増やす」と「if を増やす」は、多くの場合トレードオフ になります。

ダックタイピング — 同じメソッド名さえあればよい

Python のポリモーフィズムには、もう一段ゆるい考え方があります。それが ダックタイピング(duck typing) です。「アヒルのように歩き、アヒルのように鳴くなら、それはアヒルだ」という考え方を、Python では 「同じメソッド名さえ持っていればクラスは何でもよい」 という形で取り入れています。

たとえば下のコードでは、CatDogどちらも Animal を継承していなくてもspeak() というメソッドさえ持っていれば同じ関数で扱えます。継承関係よりも 「持っているメソッド」が一致しているか を優先する — これが Python らしいポリモーフィズムです。

class Cat:
    def speak(self):
        return "ニャー"

class Dog:
    def speak(self):
        return "ワン"

def shout(animal):                # 引数の型は問わない
    print(animal.speak())

shout(Cat())   # ニャー
shout(Dog())   # ワン

Java や C# のように 「親クラスをそろえないとポリモーフィズムが効かない」 タイプの言語と違い、Python は呼び出した瞬間にメソッドがあれば動きます。設計の自由度は上がりますが、「同じ意味でメソッド名をそろえる」 という規律は呼び出し側の責任になる — その点だけ意識しておきましょう。

2 つの設計を 1 枚にまとめると

ポリモーフィズム的な設計 vs 型分岐型の設計
ポリモーフィズム的な設計
  • 呼び出し側のコードemp.calculate_salary() の 1 行
  • 新しい型を追加 — 新クラスで calculate_salary を実装するだけ
  • 変更の影響範囲 — クラス側に閉じる
  • 可読性 — 「同じメソッド名で型ごとに違う」と読めば済む
型分岐型の設計
  • 呼び出し側のコードif type(emp) is ... の連発
  • 新しい型を追加 — 全分岐を見直す必要
  • 変更の影響範囲 — 呼び出し側にも波及
  • 可読性 — 分岐ロジックを毎回読む必要
同じ要件を実装しても、設計次第で 呼び出し側のコード量変更の影響範囲 が大きく変わる。
QUIZ

理解度チェック

まずは1問ずつ答えてみましょう。

Q1ポリモーフィズム の説明として最も適切なものはどれですか?

Q2ポリモーフィズムを使うと、呼び出し側のコードからどんな構造が消える ことが多いですか?

Q3Python の ダックタイピング に関する説明として最も適切なものはどれですか?