読み込み中...

Pythonのポリモーフィズム完全理解!5ステップで理解する方法と10の実例

Pythonのポリモーフィズムを解説するイラストとテキストのマッシュアップ Python
この記事は約29分で読めます。

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

本記事のサンプルコードを活用して機能追加、目的を達成できるように作ってありますので、是非ご活用ください。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を満たす現役のプログラマチームによって監修されています。

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

Pythonのポリモーフィズムは、同じ呼び出し方で異なるオブジェクトを扱うための考え方です。初心者ガイドとして押さえたい結論は、型名よりもspeakmake_soundなどの振る舞いに注目すると、オブジェクト指向のコードが読みやすくなる点にあります。

その考え方は、クラス継承だけでなく、ダックタイピング、抽象基底クラス、特殊メソッド、ジェネリックプログラミングにも広がります。Pythonの公式ドキュメントにも、クラス、特殊メソッド、abctypingに関する説明があり、実装判断の一次情報として参照できるのが基本です。

具体的には、同じ関数にDogCatを渡しても、両方がmake_soundを持っていれば同じ形で呼び出せます。そのため、ポリモーフィズムを理解すると、条件分岐で型を細かく分けるより、共通インターフェースをそろえる設計へ進みやすくなります。

動作確認環境
  • Python 3.12
  • 標準ライブラリ: abc / typing
📖 この記事で学べること
  • Pythonのオブジェクト指向で使うclassobjectmethodの関係
  • ポリモーフィズムとダックタイピングの違い
  • 継承、抽象基底クラス、特殊メソッドを使った実例
  • 初心者ガイドとして避けたい型まわりの落とし穴
  • チュートリアル形式で読む10の実例と注意点

関連する基礎を補う場合は、Python初心者のための完全ガイドPythonで改行を制御する方法も合わせて読むと、出力や関数呼び出しの理解がつながります。

Pythonとオブジェクト指向プログラミング

Pythonでは、数値、文字列、関数、クラスから生成したインスタンスまで、多くの値がオブジェクトとして扱われます。そのため、オブジェクト指向プログラミングを学ぶときは、classで設計を作り、__init__で初期状態を整え、selfを通じて属性やメソッドへアクセスする流れを押さえると理解しやすくなるのが目安です。

一般に、クラスは設計図、オブジェクトはその設計図から作られる具体物として説明されます。ただし、Pythonでは型が柔軟に扱われるため、クラス階層だけに依存せず、必要なメソッドを持つかどうかで処理を組み立てる場面も多くあります。

その背景を理解すると、ポリモーフィズムは難しい用語ではなく、同じ名前の操作を複数の型に適用する設計技法として整理できるのがポイントです。プログラミング学習の初期段階では、printlenstrのような組み込み関数が、異なる型に対して自然に動く点から観察するとつかみやすいでしょう。

Pythonのクラスとオブジェクト

クラスにはデータを表す属性と、振る舞いを表すメソッドをまとめられます。この構造により、関連する値と処理を一体化でき、同じ設計を使って複数のインスタンスを作れます。

class MyExampleClass:
    def __init__(self, attribute):
        self.attribute = attribute

結果: 期待される動作は、MyExampleClassから作られるインスタンスがattributeという属性を持つことです。

このコードでは、__init__がインスタンス生成時に呼ばれる初期化メソッドとして働きますが、これは押さえたい点です。一方、self.attributeに代入された値はオブジェクトごとに保持されるため、同じクラスから作ったインスタンスでも別々の状態を持てます。

公式ドキュメントによれば、クラス定義やメソッド呼び出しはPythonのデータモデルと密接に関係しています。詳しい仕様はPython公式チュートリアルのクラスPythonデータモデルで確認できるのが一般的です。

これらの基礎を押さえると、Pythonのコードでよく見るobj.method()の読み方が変わります。呼び出し側はオブジェクトの内部構造をすべて知る必要はなく、約束されたメソッドを通じて値を扱うため、プログラミング上の責務を小さく分けられます。

その分け方は、ファイル操作、画面操作、データ分析のような異なる題材にも応用できるのが現実的です。たとえば、保存先がローカルファイルでもクラウドストレージでも、saveという操作をそろえれば、呼び出し側の処理は大きく変えずに済みます。

ただし、何でも同じ名前にすればよいわけではありません。メソッド名、引数、戻り値、例外の意味が一致しているときに限り、同じインターフェースとして扱う設計が読みやすくなります。

ポリモーフィズムの基本理解

ポリモーフィズムは、同じ操作が対象に応じて異なる振る舞いをする性質を指すると整理できます。オブジェクト指向の文脈では、同じメソッド名や演算子を使いながら、実際の処理を各クラスに任せる考え方として扱われます。

これにより、呼び出し側はif isinstance(...)のような分岐を増やしすぎず、共通の呼び出し方に集中できます。ただし、どのメソッド名をそろえるかが曖昧なままだと、実行時のAttributeErrorにつながるため、設計上の約束を明確にする必要があると理解できます。

実際、同じ操作名をそろえる設計は、テストの書き方にも影響します。make_soundを持つオブジェクトを複数用意し、同じテスト関数で戻り値だけを確認できれば、クラスごとの差分を小さな単位で検証できます。

そのため、ポリモーフィズムを使う前に、対象クラスが共有する責務を短い言葉で説明できるかを確認すると判断しやすくなると覚えるとよいでしょう。説明が長くなる場合は、共通化の範囲が広すぎる可能性があります。

ポリモーフィズムとは何か

プログラミングでのポリモーフィズムは、ひとつのインターフェースで複数の型を扱う能力として説明できます。たとえば、drawというメソッド名をCircleRectangleTriangleが持っていれば、呼び出し側は図形の種類を詳しく知らなくてもshape.draw()と書けると考えられます。

その考え方は、継承を使う設計にも、継承を使わないダックタイピングにも当てはまります。初心者ガイドとしては、継承は「型の関係を明示する方法」、ダックタイピングは「必要な振る舞いを満たすかで扱う方法」と分けると整理できます。

これを別の角度から見ると、ポリモーフィズムは変更が起きやすい部分をクラス側へ寄せる技術とも言えますし、これが一つの目安です。呼び出し側の関数はanimal.make_sound()だけを知り、鳴き声の内容はDogCatが担当します。

ただし、共通化した関数が受け取るオブジェクトに過剰な前提を置くと、柔軟さは失われます。必要な操作がmake_soundだけなら、agenameのような属性まで暗黙に要求しないほうが、関数の利用範囲を保てますが、覚えておくと役立つでしょう。

ポリモーフィズムの利点

ポリモーフィズムを使うと、処理の呼び出し側と各クラスの具体的な実装を分離できます。そのため、新しい型を追加するときも、既存の関数本体を大きく変えずに、同じメソッド名を備えたクラスを増やす形にできます。

一方、柔軟さだけを優先すると、どのメソッドを持つべきかがコードから読み取りにくくなると言えるでしょう。この課題には、ABC@abstractmethodProtocol、型ヒントを組み合わせると対処しやすく、チュートリアルの後半で扱う実例にもつながります。

観点主な書き方向く場面注意点
ダックタイピングobj.fly()振る舞いだけをそろえたい処理メソッド不足は実行時に見つかります
継承class Dog(Animal)共通の親型で整理したい設計階層を深くしすぎない配慮が必要です
抽象基底クラスABC@abstractmethod実装必須のメソッドを示したい場面抽象化しすぎると小規模コードでは重くなります
特殊メソッド__add____len__演算子や組み込み関数に対応したい型読んだ人が期待する意味から外さないことが必要です
ジェネリックGeneric[T]型を保ちながら共通処理を書きたい場面実行時の型保証とは別に考えます

具体的には、ポリモーフィズムは「呼び出し方を固定し、実装を差し替える」考え方として使えます。決済処理ならpay、通知処理ならsend、変換処理ならconvertのように、役割に合う名前を決めると、クラスが増えても読み手が処理の流れを追いやすくなります。

そのため、初心者ガイドでは、ポリモーフィズムを継承の派生知識としてだけ覚えるより、関数の引数に渡せるオブジェクトの条件として理解するほうが実用に近くなるのが基本です。Pythonでは型の一致より振る舞いの一致が自然に使われる場面が多いため、この視点が設計の判断材料になります。

一方、振る舞いの一致を言葉だけで共有すると、チーム開発や長期保守では意図が薄れやすくなります。docstring、型ヒント、抽象基底クラス、テストケースを組み合わせると、同じインターフェースの意味をコード上に残せますし、ここを基本と考えるとよいでしょう。

Pythonでのポリモーフィズムの使い方

Pythonでポリモーフィズムを使う最小単位は、同じメソッド名を持つ複数のクラスを用意し、呼び出し側を共通化することです。オブジェクト指向に慣れていない段階でも、cat.speak()dog.speak()が同じ形で書ける点に注目すると、仕組みを把握しやすくなります。

このとき、呼び出し側はCatDogかを直接調べません。必要なのはspeakというメソッドがあることであり、Pythonらしいプログラミングでは、この振る舞い中心の見方がよく使われます。

基本的なポリモーフィズムの例

次のコードは、CatDogがどちらもspeakを持つ例です。戻り値はクラスごとに違いますが、呼び出し方は同じなので、これがポリモーフィズムの入口になるのが目安です。

class Cat:
    def speak(self):
        return "にゃー"

class Dog:
    def speak(self):
        return "わんわん"

cat = Cat()
dog = Dog()

print(cat.speak())  # 出力:にゃー
print(dog.speak())  # 出力:わんわん

結果: 期待される出力は、1行目に「にゃー」、2行目に「わんわん」です。

この例では、speakという同じ名前が、インスタンスの種類に応じて別の結果を返します。ただし、より実用的な設計では、個別にcat.speak()を書くより、関数やループに渡して同じ処理として扱う形が多くなります。

Pythonにおけるポリモーフィズムの10の実例

Pythonのポリモーフィズムは、単純なメソッド呼び出しから型ヒントを使うジェネリックプログラミングまで幅があるのがポイントです。ここからの実例は、初心者ガイドとして読みやすい順に、同じ呼び出し方を保ちながら処理を切り替えるパターンを並べています。

その中には、チュートリアルでよく扱われる継承や抽象基底クラスだけでなく、特殊メソッド、デコレータ、メタクラスのように少し進んだ題材も含まれます。各コードの直後に期待される出力を置くため、出力の形と設計意図を並べて確認できるのが一般的です。

これらの実例を読むときは、コードの長さより「呼び出し側が何を知らなくてよいか」に注目すると理解しやすくなります。animal_soundは動物の種類を知らず、start_flyingは対象が鳥か機械かを知らず、Stackは要素型の詳細を直接処理しません。

その構造があるため、Pythonのプログラミングでは、条件分岐を増やす前に共通メソッドへ寄せられないかを考える価値があります。ただし、共通化のために意味の違う処理を無理に同じ名前へ集めると、かえって読みにくくなるのが現実的です。

基本的に、同じ名前のメソッドは同じ目的を持つべきです。戻り値の型や副作用が大きく異なる場合は、別名にする、戻り値を統一する、例外を明示するなどの調整を行うと、ポリモーフィズムの利点を保ちやすくなります。

サンプルコード1:異なるクラスのオブジェクト

異なるクラスでも、同じメソッド名を持っていれば同じ関数へ渡せます。この実例では、animal_soundmake_soundだけを呼び出し、具体的な型には依存しません。

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

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

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

dog = Dog()
cat = Cat()

animal_sound(dog)  # ワン
animal_sound(cat)  # ニャー

結果: 期待される出力は、1行目に「ワン」、2行目に「ニャー」です。

これにより、animal_soundDog専用でもCat専用でもなく、make_soundを持つオブジェクトを受け取る関数になります。そのため、新しい動物クラスを増やすときも、同じメソッド名をそろえれば関数側の変更を抑えられます。

サンプルコード2:継承を使ったポリモーフィズム

継承を使うと、複数の具体クラスを共通の親クラスとして扱えますし、ここがポイントです。この実例では、Animalを基底クラスにして、DogCatmake_soundを上書きします。

class Animal:
    def make_sound(self):
        pass

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

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

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

dog = Dog()
cat = Cat()

animal_sound(dog)  # ワン
animal_sound(cat)  # ニャー

結果: 期待される出力は、1行目に「ワン」、2行目に「ニャー」です。

この形では、DogCatAnimalの派生クラスであることがコード上に表れます。ただし、基底クラスのmake_soundpassだけだと実装忘れに気づきにくいため、抽象基底クラスを使う方法も検討できます。

サンプルコード3:抽象クラスとポリモーフィズム

抽象基底クラスを使うと、派生クラスに実装してほしいメソッドを明示できると整理できます。Python標準のabcモジュールにはABCabstractmethodがあり、設計上の約束をコードに残せます。

from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def make_sound(self):
        pass

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

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

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

dog = Dog()
cat = Cat()

animal_sound(dog)  # ワン
animal_sound(cat)  # ニャー

結果: 期待される出力は、1行目に「ワン」、2行目に「ニャー」です。

このコードでは、Animalを直接完成したクラスとして使うのではなく、共通インターフェースを表す土台にしています。公式ドキュメントのabcモジュールにも、抽象基底クラスの用途が説明されています。

このような考え方は、Webアプリケーションの入力検証や変換処理にも応用できると理解できます。validateを持つ複数の検証クラス、renderを持つ複数の表示クラス、exportを持つ複数の出力クラスを用意すると、呼び出し側は処理順序に集中できます。

一方で、名前だけをそろえても意味が違えば設計は崩れます。renderがHTML文字列を返すクラスと、画面へ直接描画するクラスを同じ関数に渡すなら、戻り値や副作用の扱いを明記する必要があると覚えるとよいでしょう。

サンプルコード4:オペレータのオーバーロード

Pythonでは、+のような演算子も特殊メソッドで振る舞いを変えられます。__add__を実装すると、ユーザー定義クラスでも加算の意味をクラス側で定義できます。

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        if isinstance(other, Vector):
            return Vector(self.x + other.x, self.y + other.y)
        else:
            raise ValueError('Vectorクラスのオブジェクトを指定してください。')

v1 = Vector(1, 2)
v2 = Vector(2, 3)
v3 = v1 + v2
print(v3.x, v3.y)  # 出力: (3, 5)

結果: 期待される出力は「3 5」です。

この実例では、v1 + v2が内部的にv1.__add__(v2)として扱われます。一方、演算子の意味が直感から外れると読みにくくなるため、__add__には「加算」として自然に読める処理を割り当てるのが基本になります。

一般に、特殊メソッドはPythonの組み込み構文とつながるため、読み手の期待に沿った設計が求められますが、これは押さえたい点です。__len__なら要素数、__contains__なら所属判定、__eq__なら等価比較というように、標準的な意味から外さないことが大切になります。

その意味で、特殊メソッドを使ったポリモーフィズムは、単に演算子を使えるようにする話ではありません。Pythonの言語機能に自作クラスを自然に参加させ、既存の構文で同じように扱えるようにする設計です。

サンプルコード5:関数のオーバーロード

Pythonは同じ関数名で引数型だけが異なる関数を複数定義する形式を、一般的な意味では採用していません。ただし、デフォルト引数や*args**kwargsを使えば、呼び出し方に応じて処理を変える設計は可能です。

def func(x, y=None):
    if y is None:
        return x * x
    else:
        return x * y

print(func(2))      # 出力: 4
print(func(2, 3))   # 出力: 6

結果: 期待される出力は、1行目に「4」、2行目に「6」です。

このコードでは、yNoneのときは二乗、値が渡されたときは積を返します。ただし、処理が複雑になりすぎる場合は、関数名を分ける、functools.singledispatchを検討する、型ヒントを添えるといった整理が必要になります。

サンプルコード6:ダックタイピング

ダックタイピングでは、オブジェクトの型名よりも、求めるメソッドを持つかどうかに注目すると考えられます。この実例では、DuckPlaneは別の概念ですが、どちらもflyを持つため同じ関数で扱えます。

class Duck:
    def quack(self):
        return "Quack!"
    def fly(self):
        return "The duck is flying."

class Plane:
    def fly(self):
        return "The plane is flying."

def start_flying(obj):
    return obj.fly()

duck = Duck()
plane = Plane()

print(start_flying(duck))  # "The duck is flying."
print(start_flying(plane))  # "The plane is flying."

結果: 期待される出力は、1行目に「The duck is flying.」、2行目に「The plane is flying.」です。

このとき、start_flyingDuck専用の関数ではありません。その代わり、flyを持たないオブジェクトを渡すとAttributeErrorになり得るため、公開APIでは型ヒントや明示的なチェックを合わせると読み手に意図が伝わります。

同様に、Pythonでウィンドウ操作を自動化する記事のような自動化プログラミングでも、同じ操作名で複数対象を扱う発想が役立ちます。対象が画面部品でもファイルでも、必要な振る舞いをそろえる設計が保守性につながりますし、これが一つの目安です。

print(start_flying(duck))  # 出力:The duck is flying.
print(start_flying(plane))  # 出力:The plane is flying.

結果: 期待される出力は、duckでは「The duck is flying.」、planeでは「The plane is flying.」です。

この短い確認コードは、同じ関数が異なるオブジェクトのflyを呼び分ける点を切り出しています。チュートリアルとして読む場合は、型名をそろえるより、メソッド名と戻り値の意味をそろえる意識を持つと理解が進みます。

サンプルコード7:デコレータを用いたポリモーフィズム

デコレータは、関数を受け取り、別の関数で包んで返す仕組みです。ポリモーフィズムの中心例はクラスですが、Pythonでは関数もオブジェクトなので、呼び出し可能オブジェクトを同じ形で扱う発想にも広がりますが、覚えておくと役立つでしょう。

def uppercase_decorator(function):
    def wrapper():
        func = function()
        make_uppercase = func.upper()
        return make_uppercase
    return wrapper

@uppercase_decorator
def say_hi():
    return 'hello there'

print(say_hi())  # 出力:"HELLO THERE"

結果: 期待される出力は「HELLO THERE」です。

このコードでは、uppercase_decoratorsay_hiの戻り値を受け取り、upperで大文字へ変換します。ただし、元関数が文字列以外を返す場合はupperが存在しないため、入力と戻り値の約束をコメントや型ヒントで示すと扱いやすくなります。

print(say_hi())  # 出力:"HELLO THERE"

結果: 期待される出力は「HELLO THERE」です。

この確認コードだけを見ると、呼び出し側はsay_hiが装飾済みかどうかを意識せずに使えると言えるでしょう。そのため、ログ出力、権限チェック、戻り値の加工など、同じ呼び出し形を保ったまま振る舞いを足したい場面でデコレータが選ばれます。

サンプルコード8:メタクラスとポリモーフィズム

メタクラスは、クラスを作るためのクラスです。通常のチュートリアルでは頻出しませんが、クラス生成時にメソッドを追加できるため、複数クラスへ共通インターフェースを付与する実例として理解できます。

class Meta(type):
    def __new__(mcs, name, bases, attrs):
        print("メタクラスで新たなクラスを作成するのが基本です。")
        attrs['add'] = lambda self, value: setattr(self, 'value', self.value + value)
        return super().__new__(mcs, name, bases, attrs)

class Base(metaclass=Meta):
    def __init__(self, value):
        self.value = value

class Derived(Base):
    pass

base = Base(10)
derived = Derived(20)
base.add(10)
derived.add(30)
print(base.value)  # 20
print(derived.value)  # 50

結果: 期待される出力は、クラス作成時のメッセージに続き、値として「20」と「50」が表示される形です。

このコードでは、Meta.__new__がクラス生成時にaddメソッドを属性辞書へ追加します。一方、メタクラスは読み手の負担が大きいため、通常の継承やデコレータで解ける設計なら、そちらを選ぶほうが現実的です。

⚠️ 注意: メタクラスは高度な仕組みです。ライブラリやフレームワークの内部では有効ですが、アプリケーションコードでは必要性を確認してから使うのが安全です。

サンプルコード9:ジェネリックプログラミングとポリモーフィズム

ジェネリックプログラミングでは、値の型をパラメータとして扱いながら、共通の処理を定義します。PythonのtypingにはTypeVarGenericがあり、型チェッカーに意図を伝えるために利用できるのが目安です。

from typing import TypeVar, Generic

T = TypeVar('T')

class Stack(Generic[T]):
    def __init__(self):
        self.items = []

    def push(self, item: T):
        self.items.append(item)

    def pop(self) -> T:
        return self.items.pop()

stack1 = Stack[int]()
stack2 = Stack[str]()
stack1.push(1)
stack2.push("a")
print(stack1.pop())  # 1
print(stack2.pop())  # 'a'

結果: 期待される出力は、1行目に「1」、2行目に「a」です。

この実例では、Stack[int]Stack[str]が同じpushpopを持ちながら、扱う値の型を分けています。詳しい型ヒントの仕様はtypingモジュールで確認できます。

サンプルコード10:動的メソッド定義とポリモーフィズム

Pythonでは、関数もオブジェクトとして扱われるため、setattrでクラスへ後からメソッドを追加できるのがポイントです。この実例は、動的に同じ呼び出し口を作る方法を示しています。

class A:
    pass

def say_hello(self):
    return "Hello, A!"

setattr(A, "say", say_hello)

class B(A):
    pass

a = A()
b = B()
print(a.say())  # Hello, A!
print(b.say())  # Hello, A!

結果: 期待される出力は、1行目と2行目のどちらも「Hello, A!」です。

このコードでは、Aに追加したsayが派生クラスBからも使えます。ただし、動的追加は定義場所が追いにくくなるため、通常はクラス本体にメソッドを書く設計を優先し、必要な場面だけに絞ると保守しやすくなります。

関連するデータ処理の題材として、Pythonで折れ線グラフを作成する方法Pythonで表を操作するための詳細ガイドでも、同じ関数やメソッドを複数データに適用する考え方が登場するのが一般的です。こうした実例を横断して読むと、ポリモーフィズムが文法だけでなく設計の話だと理解できます。

その注意点を避けるには、実装前に「この関数が必要とする最小の振る舞い」を言語化すると有効です。必要なのがsoundだけなら親クラス全体を求める必要はなく、逆に初期化手順や共通状態まで共有したいなら継承が向く場合もあります。

一方、外部ライブラリへ渡すクラスを作る場合は、ライブラリが求める特殊メソッドやプロトコルに合わせる必要があるのが現実的です。たとえば、長さを返すなら__len__、反復可能にするなら__iter__、文字列表現を整えるなら__repr____str__を検討します。

こうした設計判断は、単なる文法暗記では身につきにくい部分です。実例ごとに、呼び出し側、受け取るオブジェクト、共通メソッド、戻り値の意味を対応づけると、チュートリアルとしての理解が深まります。

Pythonでのポリモーフィズムの注意点と対策

Pythonのポリモーフィズムは柔軟ですが、型やインターフェースの約束が曖昧になりやすい面があると整理できます。そのため、初心者ガイドとして特に押さえたいのは、実行時エラーを防ぐために、求めるメソッド名、戻り値、例外の扱いを明確にすることです。

一方、すべてを厳密にしようとして抽象クラスを増やしすぎると、小さなプログラムでは読みにくさが勝つ場合もあります。オブジェクト指向の設計では、ダックタイピングで十分な範囲と、ABCや型ヒントで約束を明示すべき範囲を分ける判断が必要になります。

これらの注意点を踏まえると、ポリモーフィズムを導入する基準は「分岐を減らせるか」だけでは足りません。新しい型を追加したときに既存コードへ触る範囲が減るか、同じ名前の操作が同じ意味で読めるか、エラー時の挙動を説明できるかまで確認すると判断しやすくなると理解できます。

その確認には、短い利用例を先に書く方法が向いています。for animal in animalsで回し、各要素にsoundを呼ぶ形が自然に読めるなら、共通インターフェースとしてまとめる価値があります。

これに加えて、ポリモーフィズムを使う関数では、受け取ったオブジェクトを内部で作り直さないことも読みやすさに関わりますし、ここを基本と考えるとよいでしょう。外から渡されたインスタンスのメソッドを呼ぶ形にすると、依存関係が見えやすく、テスト用の代替オブジェクトも渡しやすくなります。

その考え方は、Pythonのプログラミングでよく使われる依存性の注入にもつながります。ファイル出力、ログ出力、通知処理などを同じインターフェースで差し替えられるようにすると、実例の小さなコードから実務的な設計へ発展させやすくなると覚えるとよいでしょう。

ただし、差し替えやすさだけを優先すると、何を受け取れる関数なのかが曖昧になります。受け取る側の名前、型ヒント、短い利用例をそろえることで、ポリモーフィズムの柔軟さと読みやすさを両立できます。この整理は、初心者ガイドとして復習するときにも役立ち、再利用設計にも効きますし、ここがポイントです。

型チェックの欠如

Pythonは動的型付け言語であり、関数へ渡される値の型は実行時に決まりますし、ここがポイントです。そのため、animal_soundquackを呼ぶ設計なら、引数がquackを持つことが前提になります。

ただし、この前提はコードを読むだけでは見落とされることがあります。型ヒント、単体テスト、抽象基底クラス、またはhasattrによる確認を組み合わせると、期待する振る舞いを読み手に示しやすくなると考えられます。

class Duck:
    def quack(self):
        return "クワッキー"

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

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

duck = Duck()
dog = Dog()

animal_sound(duck)  # "クワッキー"を出力
animal_sound(dog)   # "ワンワン"を出力

結果: 期待される出力は、1行目に「クワッキー」、2行目に「ワンワン」です。

この実例では、DuckDogの関係は継承で結ばれていません。それでも両方にquackがあるため、animal_soundは同じ形で呼び出せます。

ℹ️ 補足: 型ヒントは実行時の型チェックを自動で行う仕組みではありません。mypypyrightなどの静的解析ツールと併用すると、設計上のミスを早めに見つけやすくなります。

具体的には、呼び出し側の関数名を先に決めると、クラスへ持たせるメソッドも整理しやすくなると言えるでしょう。notify(user)が必要なら通知クラスにはsendを、serialize(value)が必要なら変換クラスにはto_jsonto_dictを持たせるように、用途から逆算できます。

その設計では、ポリモーフィズムの対象になるメソッドを小さく保つことも大切になります。ひとつのメソッドが読み込み、変換、保存、通知まで担当すると、別クラスで同じ名前を使っても責務が広がりすぎますが、これは押さえたい点です。

ただし、すべてを細かく分割すると、逆に呼び出し側が複数メソッドの順序を知る必要があります。共通インターフェースは、読み手が一文で説明できる粒度に置くと、Pythonのオブジェクト指向として扱いやすくなります。

明確なインターフェイスの欠如

明確なインターフェースがないと、どのクラスがどのメソッドを実装すべきかが曖昧になるのが基本です。チュートリアル段階では小さな例でも動きますが、プログラミングの規模が大きくなるほど、約束をコードで表す価値が増します。

その対策として、抽象基底クラスを使うと、派生クラスにsoundの実装を求められます。抽象メソッドを残したままインスタンス化しようとするとエラーになるため、実装漏れに気づきやすい構造になるのが目安です。

from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def sound(self):
        pass

class Duck(Animal):
    def sound(self):
        return "クワッキー"

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

結果: 期待される動作は、DuckDogsoundを実装した具体クラスとして扱えることです。

これにより、Animalを継承するクラスにはsoundが必要だと読み取れます。ただし、抽象基底クラスを使うだけでは戻り値の意味や例外方針までは表しきれないため、関数名、型ヒント、テスト名を合わせて意図を補うと堅実です。

具体的には、def sound(self) -> strのように戻り値を示し、呼び出し側ではIterable[Animal]のように扱う対象を表せます。一方、継承関係を持たないクラスも受け入れたい場合は、Protocolを使う設計も候補になるのがポイントです。

💡 Tips: 小さなスクリプトではダックタイピング、大きな設計ではABCProtocolというように、制約の強さを段階的に選ぶと読みやすくなります。

一方、学習段階で混乱しやすいのは、継承を使うこと自体が目的になってしまう点です。ポリモーフィズムで本当に必要なのは、呼び出し側から見た操作の統一であり、継承はそのための選択肢のひとつにすぎません。

そのため、チュートリアルのコードを読むときは、親クラスの有無だけで分類せず、同じメソッド名、同じ引数、同じ戻り値の意味が保たれているかを確認すると効果的です。そこがそろっていれば、Pythonの柔軟な型システムを活かした設計として理解できます。

まとめ

Pythonのポリモーフィズムは、同じ呼び出し方で異なるオブジェクトを扱う設計技法です。オブジェクト指向プログラミングでは、継承、抽象基底クラス、特殊メソッド、ダックタイピングを使い分けることで、呼び出し側のコードをすっきり保ちやすくなるのが一般的です。

その中心にあるのは、型名ではなく振る舞いをそろえる考え方です。make_soundfly__add__popのような共通の操作を設計すると、同じ関数が複数のクラスを扱えるようになります。

ただし、柔軟さには実行時エラーのリスクもあります。初心者ガイドとしては、最初にシンプルなダックタイピングを理解し、規模が大きくなったらABC@abstractmethodTypeVarGenericProtocolなどで約束を明確にする流れが扱いやすいでしょう。

これらの実例を通じて、ポリモーフィズムは文法名を覚えるだけの題材ではなく、変更に強いプログラミングへ近づくための設計判断だと整理できるのが現実的です。チュートリアルのコードを自分のクラス名やメソッド名に置き換えながら読むと、Pythonらしいオブジェクト指向の感覚が定着します。

関連記事

著者: Japanシーモア編集部

Japanシーモアは、Web/IoT/APP/SYS 分野のプログラミング情報を体系的に提供するメディアです。本記事は編集部による執筆とAI支援を組み合わせて制作し、公開前に編集部が校正しています。誤りや改善案がございましたらお問い合わせよりご連絡ください。

※本記事は実在のエンジニア複数名で構成される Japanシーモア編集部が、AI支援を活用して作成・校正・公開しています。