Q1ポリモーフィズムの説明として最も適切なものはどれですか?
ポリモーフィズム — 同じ名前で型ごとに違う動きをさせる
Pythonのポリモーフィズムを給与計算と通知クラスで解説。同名メソッドを子ごとにオーバーライドし、リストに混在させても1行で処理する設計、if type(...)分岐を消す方法、ダックタイピングを確認できます。
前回までで継承と MRO の仕組みを押さえました。仕上げとして、オブジェクト指向の三大要素のひとつ — ポリモーフィズム(polymorphism)を整理します。
ポリモーフィズムとは
ポリモーフィズム(polymorphism)は「同じインターフェース(メソッド名)で、型ごとに違う動きをする」という考え方です。たとえば「給与を計算する」という処理を、従業員(Employee)・マネージャー(Manager)・エンジニア(Engineer)のそれぞれで違う計算式にしたい、というケースを考えます。
親クラスEmployeeにcalculate_salaryメソッドを置いて、子クラスManagerとEngineerは同じ名前のメソッドを自分用の計算式でオーバーライドします。こうしておくと、呼び出す側は「どのクラスかを気にせず employee.calculate_salary() と書くだけ」で、それぞれに合った計算が実行されます。
Employeeにcalculate_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
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
employees = [...](種類が混ざっている)for emp in self.employees:で順に処理- emp.calculate_salary() を呼ぶだけ
- base_salary を返す
- base + team * 5万
- base + skill * 2万
PayrollSystem側のコードは「同じメソッド名で呼ぶ」だけで型ごとに正しい計算が走る。ポリモーフィズムを使わなかった場合の比較
もしポリモーフィズムを使わずに同じことを書こうとすると、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 行で済む
if typeが並び、新しい型が増えるたびに分岐を増やす必要がある。ポリモーフィズム型は呼び出し側を 1 行に保ったまま、新しい型はクラスを足すだけで対応できる。「呼び出し側は型を気にしない」が合言葉
ポリモーフィズムが効いている設計かどうかは、呼び出し側のコードに if type(...) や if isinstance(...) が並んでいないかで判断できます。並んでしまっているなら、その分岐をクラス側のメソッドオーバーライドに引っ越すのがリファクタリングの定石です。「クラスを増やす」と「ifを増やす」は、多くの場合トレードオフになります。
ダックタイピング — 同じメソッド名さえあればよい
Python のポリモーフィズムには、もう一段ゆるい考え方があります。それがダックタイピング(duck typing)です。「アヒルのように歩き、アヒルのように鳴くなら、それはアヒルだ」という考え方を、Python では「同じメソッド名さえ持っていればクラスは何でもよい」という形で取り入れています。
たとえば下のコードでは、CatとDogはどちらも 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 枚にまとめると
- 呼び出し側のコード —
emp.calculate_salary()の 1 行 - 新しい型を追加 — 新クラスで
calculate_salaryを実装するだけ - 変更の影響範囲 — クラス側に閉じる
- 可読性 — 「同じメソッド名で型ごとに違う」と読めば済む
- 呼び出し側のコード —
if type(emp) is ...の連発 - 新しい型を追加 — 全分岐を見直す必要
- 変更の影響範囲 — 呼び出し側にも波及
- 可読性 — 分岐ロジックを毎回読む必要
理解度チェック
まずは1問ずつ答えてみましょう。
Q2ポリモーフィズムを使うと、呼び出し側のコードからどんな構造が消えることが多いですか?
Q3Python のダックタイピングに関する説明として最も適切なものはどれですか?