クラス

【Python】ポリモーフィズムの実現方法

【Python】ポリモーフィズムの実現方法
naoki-hn

Python におけるポリモーフィズムの実現方法について解説します。

ポリモーフィズム(polymorphism)

クラス実装において、異なるクラスが共通のインターフェース(メソッドやプロパティ)を実装し、同じメソッド名で異なる振る舞いをすることをポリモーフィズム(多様性・多相性)と言います。

Python では、ポリモーフィズムを実現する手法として、以下の 2 つのアプローチがあります。

  1. 抽象クラスの継承:abc.ABC によるポリモーフィズム
  2. 構造的部分型:typing.Protocol によるポリモーフィズム

この記事では、Python でポリモーフィズムを実現する方法を紹介します。

抽象クラスの継承:abc.ABC

以降では、Person、Teacher、Doctor クラスを用いてポリモーフィズムを考えていきます。まずは、継承をベースにした考え方について見ていきましょう。以下は、Person、Teacher、Doctor クラスの継承関係を表した図です。

python ポリモーフィズム

mywork メソッドは、仕事の内容を出力するメソッドです。しかし、同じ mywork メソッドであっても、Teacher クラスでは教える仕事、Doctor クラスでは診察する仕事であることを説明するように出力の振る舞いを変えたいとします。

このような際にはメソッドのオーバーライドを行います。オーバーライドは「クラスの継承の基本」を参考にしてください。オーバーライドは、任意でメソッドの挙動を変更する仕組みですが、派生クラスでメソッドの実装を強制することまではできません。

派生クラスでのメソッドの実装を強制したい場合は「抽象クラス」「抽象メソッド」を使用します。抽象クラスはインスタンス化できないクラスであり、クラス設計の構造を提供します。抽象メソッドは、抽象クラスを継承する派生クラスが具体的に実装を行う必要があるメソッドのことを言います。

抽象クラスを継承したクラスは、抽象メソッドを実装しない限りインスタンス化できません。そのため、抽象クラスを利用することで、派生クラスに共通のインターフェース(メソッド構造)を強制でき、設計上の一貫性や拡張性を保つことができます。

抽象クラスや抽象メソッドは、ポリモーフィズムを活かしたインターフェース設計に欠かせない概念です。

abc モジュールを用いた抽象クラス・抽象メソッドの実装

Python では、抽象クラスと抽象メソッドを実現するために abc モジュールが使用できます。abc とは「abstract base class」の略です。

以下の例は、abc モジュールを使用して抽象クラス、抽象メソッドを定義しています。

import abc


class Person(abc.ABC):
    def __init__(self, name: str) -> None:
        self.__name = name

    @property
    def name(self) -> str:
        return self.__name

    @name.setter
    def name(self, value: str) -> None:
        self.__name = value

    def say_myname(self) -> None:
        print(f"私の名前は、{self.__name}です。")

    @abc.abstractmethod
    def mywork(self) -> None:
        ...
        # pass でもよい


class Teacher(Person):
    def __init__(self, name: str, subject: str) -> None:
        super().__init__(name)
        self.__subject = subject

    @property
    def subject(self) -> str:
        return self.__subject

    @subject.setter
    def subject(self, value: str) -> None:
        self.__subject = value

    def mywork(self) -> None:
        print(f"私の仕事は、{self.__subject}の教師です。")


class Doctor(Person):
    def __init__(self, name: str, medical_speciality: str) -> None:
        super().__init__(name)
        self.__medical_speciality = medical_speciality

    @property
    def medical_speciality(self) -> str:
        return self.__medical_speciality

    @medical_speciality.setter
    def medical_speciality(self, value: str) -> None:
        self.__medical_speciality = value

    def mywork(self) -> None:
        print(f"私の仕事は、{self.__medical_speciality}の医者です。")


def introduction(person: Person) -> None:
    """Person 型の引数を受け取る"""
    print("=== introduction ===")
    person.say_myname()
    person.mywork()


def main():
    person1 = Teacher("田中太郎", "英語")
    person1.mywork()
    introduction(person1)

    print("==================================")

    person2 = Doctor("鈴木一郎", "内科")
    person2.mywork()
    introduction(person2)


if __name__ == "__main__":
    main()
【実行結果】
私の仕事は、英語の教師です。
=== introduction ===
私の名前は、田中太郎です。
私の仕事は、英語の教師です。
==================================
私の仕事は、内科の医者です。
=== introduction ===
私の名前は、鈴木一郎です。
私の仕事は、内科の医者です。

上記例での Personクラスは abc.ABC クラスを継承することで抽象クラスになります。mywork メソッドは @abc.abstractmethod デコレータで付けることで抽象メソッドとなり、これによりサブクラスで必ず実装されることを要求します。

抽象メソッドの定義では、継承したクラス側で実装をするため中身は不要で「...」と記載しておきます。書籍やブログなどでは「pass」と記載している例がありますが、 もちろん pass でも問題ありません。ただし、モダンな Python では「...」と書くのが一般的になってきています。

Teacher クラスと Doctor クラスは Person を継承し、mywork メソッドをオーバーライドします。もし、Doctor クラスで mywork メソッドの実装を忘れると、以下のようなエラーが発生します。

【実行結果例】
Traceback (most recent call last):
...(省略)...
TypeError: Can't instantiate abstract class Doctor with abstract methods mywork

このように abc モジュールを活用することで、抽象メソッドの実装を強制し、ポリモーフィズムを実現できます。

抽象クラスにおけるこの特徴は、インターフェース(共通の振る舞い)を定義する際に非常に重要な意味を持ちます。以下の introduction 関数は Person を引数に取る関数です。しかし、呼び出し時には Teacher や Doctor クラスのインスタンスを受け取ることができていることが分かります。

def introduction(person: Person) -> None:
    """Person 型の引数を受け取る"""
    print("=== introduction ===")
    person.say_myname()
    person.mywork()
def main():
    person1 = Teacher("田中太郎", "英語")
    person1.mywork()
    introduction(person1)

    print("==================================")

    person2 = Doctor("鈴木一郎", "内科")
    person2.mywork()
    introduction(person2)

このように「共通の型に基づいて異なるクラスを同じように扱う」ことが可能になる点は、インターフェース設計の中心となる考え方です。これがまさにポリモーフィズムであり、抽象クラスが果たす重要な役割です。

構造的部分型:typing.Protocol

上記では、abc モジュールを用いて抽象クラスを継承し、サブクラスごとにメソッドを実装させることでポリモーフィズムを実現する方法を紹介しました。

近年では、もう 1 つのポリモーフィズムの実現方法として typing.Protocol を使用した方法も広く利用されており、Python における強力なインターフェース手法となっています。

この方法は abc による手法と異なり「クラスの継承が不要」である点が大きな特徴です。まずは、Protocol のベースとなる考え方を説明し、具体的な実装方法を見ていきましょう。

構造的部分型(Structural Subtyping)

まずは、Protocol の基盤となる考え方である「構造的部分型(Structural Subtyping)」について説明します。

abc.ABC を用いた抽象クラスでは、親クラス(抽象クラス)を継承し、抽象メソッドをオーバーライドすることでポリモーフィズムを実現します。一方で、Protocol が採用しているのは構造的部分型という考え方です。

構造的部分型とは「クラス継承をしていなくても、必要なメソッド・プロパティを持っていれば、その型として扱ってよい」という仕組みであり、Python がもともと持っているダックタイピングと非常に相性が良い手法です。

例えば、 Person と Teacher、Doctor の例に当てはめてみると、以下の特徴を持つクラスは継承していなくても Person 型であると扱うことができます。

  • name プロパティ
  • say_myname メソッド
  • mywork メソッド

typing.Protocol による実装

以下で、具体的な実装例を用いて Protocol の理解を深めましょう。以下は、abc.ABC を用いた方法と出力結果自体は同じですが、背後にある考え方は異なるため説明をしていきます。

from typing import Protocol


# Protocol: 構造的部分型を使ったインターフェース
class Person(Protocol):
    @property
    def name(self) -> str: ...

    @name.setter
    def name(self, value: str) -> None: ...

    def say_myname(self) -> None: ...

    def mywork(self) -> None: ...


# Teacher クラス (Person は継承しない)
class Teacher:
    def __init__(self, name: str, subject: str) -> None:
        self._name = name
        self._subject = subject

    @property
    def name(self) -> str:
        return self._name

    @name.setter
    def name(self, value: str) -> None:
        self._name = value

    def say_myname(self) -> None:
        print(f"私の名前は、{self._name}です。")

    def mywork(self) -> None:
        print(f"私の仕事は、{self._subject}の教師です。")


# Doctor クラス (Person は継承しない)
class Doctor:
    def __init__(self, name: str, medical_speciality: str) -> None:
        self._name = name
        self._medical_speciality = medical_speciality

    @property
    def name(self) -> str:
        return self._name

    @name.setter
    def name(self, value: str) -> None:
        self._name = value

    def say_myname(self) -> None:
        print(f"私の名前は、{self._name}です。")

    def mywork(self) -> None:
        print(f"私の仕事は、{self._medical_speciality}の医者です。")


def introduction(person: Person) -> None:
    """Person 型の引数を受け取る"""
    print("=== introduction ===")
    person.say_myname()
    person.mywork()


def main() -> None:
    person1 = Teacher("田中太郎", "英語")
    person1.mywork()
    introduction(person1)

    print("==================================")

    person2 = Doctor("鈴木一郎", "内科")
    person2.mywork()
    introduction(person2)


if __name__ == "__main__":
    main()

abc.ABC を用いた方法と異なる点は以下の通りです。

  1. 継承が不要
    Teacher や Doctor が Person を継承していませんが、必要なメソッドをすべて備えているため、Person の構造的部分型として扱われます。そのため、introduce 関数に引数で渡しても問題なく動作します。
  2. メソッドの実装強制は行われない
    abc.ABC では抽象メソッドが未実装の場合、実行時にエラーが発生しましたが、Protocol では、実行時に強制しません。
  3. Protocol はインターフェース定義
    Protocol はインターフェース定義が目的であるため、abc.ABC のように親クラスで共通の具体メソッドを定義し、子クラスで使うことはできません。
  4. 既存クラスにも後付けで適用しやすい
    継承していない既存クラスにも、必要なメソッドを追加すれば、Protocol を適用できます。これは、Protocol の大きな利点です。

このように、Protocol で構造的部分型に基づくポリモーフィズムを実現できます。

abc.ABC と typing.Protocol の使い分け

ポリモーフィズムを実現する方法として「abc.ABC を使用する方法」と「typing.Protocol で構造的部分型を使用する方法」を紹介しました。どちらの方法を使用するかは、適用ケースにより十分な検討が必要ですが、使い分けの判断材料となる考え方を紹介します。

継承が必要かどうか

親クラスで共通メソッドを記載したい場合や、子クラスで必ず実装させたいメソッドがある場合は、ABC を使用しましょう。一方で、既存クラスに後付けで「この型として扱いたい」というような場合には、メソッドの定義を追加すればよいだけですので Protocol の使用がおすすめです。

実行時に強制させたいか

ABC では、抽象メソッドを実装していないとインスタンス化の際にエラーとなるため強制力があります。実行時の保証が必要なケースは ABC を使う方が良いでしょう。Protocol の場合は、インスタンス化する際にはチェックはされないため、開発者が十分に注意して実装する必要になります。

複数の型にまたがる後付けのインターフェースが必要

Protocol は、定義したメソッドを実装しているかどうかで判断されます。そのため、ライブラリのクラスや既存コードなどに後付けでインターフェースを追加することで「この Protocol を満たしている」とみなすことができます。これは、ABC では不可能な優れた点です。

設計パターンにより判断する

ABC は、オブジェクト指向ベースの設計思想です。一方で、Protocol は、Haskell や Rust のような型システムを重視したプログラミング言語の設計思想に近い部分があります。設計方針や思想に応じて使い分けるのもよいと思います。

まとめ

Python でのポリモーフィズムの実現方法について解説しました。Python では、ポリモーフィズムを実現する手法として、以下の 2 つのアプローチがあります。

  1. 抽象クラスの継承:abc.ABC によるポリモーフィズム
  2. 構造的部分型:typing.Protocol によるポリモーフィズム

この記事では、各方法の実装例や特徴について紹介しました。ポリモーフィズムはインターフェースの設計などにおいて非常に重要な役割を果たすものであるため、各方法についてしっかりと理解してもらえたらと思います。

abc の公式ドキュメントはこちらを参照してください。

typing の公式ドキュメントはこちらを参照してください。

ソースコード

上記で紹介しているソースコードについては GitHub にて公開しています。参考にしていただければと思います。

あわせて読みたい
【Python Tech】プログラミングガイド
【Python Tech】プログラミングガイド
ABOUT ME
ホッシー
ホッシー
システムエンジニア
はじめまして。当サイトをご覧いただきありがとうございます。 私は製造業のメーカーで、DX推進や業務システムの設計・開発・導入を担当しているシステムエンジニアです。これまでに転職も経験しており、以前は大手電機メーカーでシステム開発に携わっていました。

プログラミング言語はこれまでC、C++、JAVA等を扱ってきましたが、最近では特に機械学習等の分析でも注目されているPythonについてとても興味をもって取り組んでいます。これまでの経験をもとに、Pythonに興味を持つ方のお役に立てるような情報を発信していきたいと思います。どうぞよろしくお願いいたします。

※キャラクターデザイン:ゼイルン様
記事URLをコピーしました