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

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

Pythonのポリモーフィズムを給与計算と通知クラスで解説。同名メソッドを子ごとにオーバーライドし、リストに混在させても1行で処理する設計、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 のダックタイピングに関する説明として最も適切なものはどれですか?