<一覧に戻る

ポリモーフィズム(多態性)の基本

「同じ関数で、いろいろなオブジェクトをまとめて扱えたら便利じゃない?」そんなときに力を発揮するのが、オブジェクト指向のポリモーフィズム(多態性)です。

Pythonの学習を始めたばかりでも、同じ名前のメソッドに、クラスごとに違う振る舞いをさせるというイメージさえ掴めれば、すぐに日常のコードで使いこなせます。 ここでは、初心者でも迷わないように、具体例と一緒にわかりやすく解説します。

ポリモーフィズムとは?同じメソッド名で、クラスごとに違う動作

ポリモーフィズム(多態性)は、異なるクラスのインスタンスが同じメソッド名を持ち、それぞれが「自分らしい」動作をすることを指します。

大事なポイントは2つ。 「同じメソッド名を用意すること」と「中身の実装はクラスごとに違ってよいこと」。 これにより、呼び出し側のコードは何が来ても、この名前のメソッドが呼べるという前提でシンプルに書けます。結果として、コードの柔軟性、再利用性、拡張性がぐっと上がります。

実際のサンプルコードからメリットを体感してみましょう。

基本のサンプルコード

以下は、もっとも素直なポリモーフィズムの例です。 共通のメソッド名 speak を用意し、クラスごとに鳴き声が変わるイメージで実装します。

class Animal:
    def speak(self):
        raise NotImplementedError("このメソッドはサブクラスで実装する必要があります。")

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

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

def animal_sound(animal):
    print(animal.speak())

# インスタンスを作成
dog = Dog()
cat = Cat()

# ポリモーフィズムを利用
animal_sound(dog)  # 出力: ワンワン
animal_sound(cat)  # 出力: ニャー

このコードでは、Animal は「動物なら speak を持っているはずだよ」という“契約”だけを示し、具体的な鳴き声はサブクラス側(Dog と Cat)が決めています。

animal_sound 関数は「渡されたものが speak できるなら鳴かせる」という視点だけで書かれていて、Dog でも Cat でも同じ呼び出しで動きます。もし新しく Bird クラスを追加しても、speak を実装するだけで animal_sound は何も変えずに使い続けられます。

Pythonらしさを体感:継承なしでも動く「ダックタイピング」

「ところで、本当に継承しないとダメ?」と疑問に思った方もいるはず。

Pythonではダックタイピングといって、「アヒルのように鳴くなら、それはアヒルとして扱える」という考え方が強く使われます。

つまり、Animal を継承していなくても、speak メソッドさえあれば同じ関数で扱えるのです。

class Animal:
    def speak(self):
        raise NotImplementedError("このメソッドはサブクラスで実装する必要があります。")

class Robot:
    def speak(self):
        return "ピーガガ"

def animal_sound(animal):
    print(animal.speak())

robot = Robot()
animal_sound(robot)  # 出力: ピーガガ

Robot は Animal を継承していませんが、speak を持っているので問題なく動きます。これが Python のポリモーフィズムの実践的な強み。型や継承関係に縛られすぎず、必要なメソッドがあるかに注目して設計できます。

「でも、型を厳密にチェックしたいときはどうすればいい?」と感じたら、Python 3.8以降で使える「Protocol(プロトコル)」という仕組みが役に立ちます(詳しくは「インターフェースとしてのプロトコル」セクションで解説します)。

必要に応じて型ヒントで“speak を持つもの”を表現でき、静的解析ツールでチェックできます。

もう一歩実践へ:拡張に強い「共通インターフェース」の設計

現場では「あとから新しい機能を差し替えたい」「別サービスを追加したい」といった場面がよくあります。

ポリモーフィズムを使うと、呼び出し側はインターフェース(メソッド名)だけを信じて書けばよく、個別の違いはクラス側に閉じ込められます。

例えば支払い処理を考えてみましょう。クレジットカード、PayPay、Apple Pay…と対応手段が増えても、呼び出し側は process という同じメソッドを呼ぶだけで済みます。

class PaymentProcessor:
    def process(self, amount: int) -> str:
        raise NotImplementedError

class CreditCard(PaymentProcessor):
    def process(self, amount: int) -> str:
        return f"クレジットカードで{amount}円を決済しました"

class PayPay(PaymentProcessor):
    def process(self, amount: int) -> str:
        return f"PayPayで{amount}円を決済しました"

def checkout(processor: PaymentProcessor, amount: int) -> None:
    print(processor.process(amount))

checkout(CreditCard(), 1200)
checkout(PayPay(), 980)

新しい決済手段を追加したいときは、そのクラスに process を実装するだけ。checkout のコードは一切触らなくてよいので、安全に拡張できます。 「呼び出し側は何をするかではなく、どう呼ぶか(メソッド名)だけを知っていればよい」という設計は、テストもしやすく、保守がとても楽になります。

コード解説のコツ:読み手は「何を保証しているか」を知りたい

初心者のうちは「どのクラスが親で、どこでオーバーライドして…」と手続きに目が行きがちです。

ですが、本質は「この関数は、渡されたオブジェクトが speak(または process)を持っていると仮定している」という“約束”のほう。つまり、呼び出し側はインターフェースにだけ依存しているのがポイントです。

実装の違いはオブジェクトの中に閉じ込められているため、関数の中身はいつまでもシンプルなまま保てます。新しいクラスを追加しても、関数は変えなくてよい。この気持ちよさを体験すると、ポリモーフィズムを使う理由が腹落ちしてきます。

テストでも大活躍:フェイク実装で置き換えやすい

「本番の処理を呼びたくないけど、振る舞いはテストしたい」そんなときも、ポリモーフィズムが助けてくれます。 共通のメソッド名だけ合わせた“テスト用のダミークラス”を用意すれば、簡単に差し替えできます。

class FakeProcessor:
    def process(self, amount: int) -> str:
        return f"[TEST] {amount}円を処理しました"

def checkout(processor, amount: int) -> None:
    # ダックタイピング:process があればOK
    print(processor.process(amount))

checkout(FakeProcessor(), 500)

継承していなくても process を持っていれば動くので、テストが軽く、速く、書きやすくなります。

ポリモーフィズムの利点(まとめておさらい)

  • 【コードの柔軟性が上がる】:呼び出し側は「同じメソッド名」を呼ぶだけ。具体的な違いは各クラスに任せられるので、関数はスリムなまま保てます。
  • 【拡張しやすい】:新しいクラスを追加するだけで、新しい振る舞いを差し込めます。既存の関数やロジックを変更せずに拡張でき、安全です。
  • 【再利用と保守が楽になる】:共通のインターフェースでそろえることで、似た問題に同じ枠組みを再利用できます。テストの準備も簡単になります。

「最初はちょっと抽象的かも…」と思った方も、同じメソッド名をそろえるだけ、という視点でコードを書いてみると、すぐに実感できます。

まとめ

ポリモーフィズム(多態性)は、「同じメソッド名で、クラスごとの違いを吸収する」ための基本テクニックです。Pythonではダックタイピングのおかげで、継承していなくても“そのメソッドを持つなら使える”という柔軟さがあります。

  • 同じ名前のメソッドをそろえる
  • 呼び出し側はインターフェースだけに依存する
  • 新しいクラスを追加しても呼び出し側はそのまま

この3点を意識するだけで、読みやすく拡張しやすいコードにぐっと近づけます。

出力結果: