0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Python文法講義~ポリモーフィズム~

Last updated at Posted at 2025-09-16

まえがき

今回はポリモーフィズム(多態性)について扱う。

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

ポリモーフィズム:同じ名前のメソッドで異なる挙動を実現する仕組み

以下に具体的な例を示す。Triangle, RectangleクラスはいずれもFigureクラスを継承しており、それぞれ同名のget_areaメソッドを定義している。

form typing import override

class Figure():
    # width(幅)、height(高さ)を準備
    def __init__(self, width: float, heigth: float) -> None:
        self.width = width
        self.height = height

    # 面積を取得(中身はダミー)
    def get_area(self) -> float:
        return 0.0

class Triangle(Figure):
    # 三角形の面積を求めるためのget_areaメソッドを定義
    @override
    def get_area(self) -> float:
        return self.width * self.height / 2

class Rectangle(Figure):
    # 四角形の面積を求めるためのget_areaメソッドを定義
    @override
    def get_area(self) -> float:
        return self.width * self.height

if __name__ == '__main__'
    t = Triangle(10, 15)
    r = Rectangle(10, 15)
    print(t.get_area())    # 75.0
    print(r.get_area())    # 150

get_areaメソッドは、図形の面積を求めるためのメソッド。上の例では、Figureクラスを継承する2個の派生クラスTriangle/Rectangleで同名のget_areaメソッドをオーバーライドし、それぞれ三角形と四角形の面積を求めるようにしている。
このコードのポイントは、複数のクラスで同じ名前のメソッドを定義しているという点(これがポリモーフィズム)。
→ 同じ目的の機能を呼び出すために異なる名前/命令を覚えなくてもよいという点(メリット)。

ただし、上記程度の内容であれば、ポリモーフィズムは不要ともいえる。なぜなら、オーバーライドの機能のみで、最低限のポリモーフィズムの実現は可能。極論、同名メソッドさえ定義されていれば、Triangle/Rectangleクラスを独立したクラスとして定義してもかまわない(継承すら不要)。
しかし、この状態ではTriangle/Rectangleクラスがget_areaメソッドを実装することを保証できない。基底クラスFigureは、派生クラスがget_areaメソッドをオーバーライドすることを期待している(コメントなどでもその意図を表明することは可能だが、オーバーライドを強制するものではない)。
ここで活用するべきなのが抽象メソッド。

抽象メソッド

抽象メソッド:それ自体は機能を持たない空のメソッド
→ 派生クラスによって外から機能を与える。
抽象クラス:抽象メソッドを含んだクラス
→ 抽象クラスを継承したクラスは、すべての抽象メソッドをオーバーライドしなければならない(オーバーライドしない場合は、自身も抽象クラスとして、更に派生クラスとしてオーバーライドしてもらわなければならない)

全ての抽象メソッドをオーバーライドしていなければ、派生クラスはそもそもインスタンス化することすら出来ない(もちろん、抽象クラスそのものをインスタンス化することも禁止)。抽象メソッドによって、特定のメソッドが派生クラスでオーバーライドされることを保証される。

from typing import override
from abc import ABC, abstractmethod

class Figure(ABC):
    # 幅と高さを準備
    def __init__(self, width: float, height: float) -> None:
        self.width = width
        self.height = height

    # 面積を取得する抽象メソッド
    @abstractmethod
    def get_area(self) -> float:
        """サブクラスで必ず実装すべき抽象メソッド"""
        pass


class Triangle(Figure):
    # 三角形の面積を求めるためのget_areaメソッドを定義
    @override
    def get_area(self) -> float:
        return self.width * self.height / 2


class Rectangle(Figure):
    # 四角形の面積を求めるためのget_areaメソッドを定義
    @override
    def get_area(self) -> float:
        return self.width * self.height


if __name__ == '__main__':
    t = Triangle(10, 15)
    r = Rectangle(10, 15)
    print(t.get_area())    # 75.0
    print(r.get_area())    # 150.0

抽象基底クラス/抽象メソッドを定義するには、
・abcモジュールのABCクラスを継承する
・対象のメソッドを@abstractmethodデコレ―タで修飾する

抽象メソッドは派生クラスでのオーバーライドを想定しているので、基底クラスでは空のブロックとしている(pass)

また、この状態で、派生クラスTriangle/Rectangleからget_areaメソッドを取り除くと、TypeErrorを吐く。

isinstance関数

ポリモーフィズムの性質を利用するうえで、オブジェクトが実装している機能(抽象クラス)を確認することは重要。

isinstance関数:引数objの型が、引数clazzのインスタンス、またはclazz派生クラスのインスタンスである場合にTrueを返す。

from typing import override
from abc import ABC, abstractmethod

class Figure(ABC):
    # 幅と高さを準備
    def __init__(self, width: float, height: float) -> None:
        self.width = width
        self.height = height

    # 面積を取得する抽象メソッド
    @abstractmethod
    def get_area(self) -> float:
        """サブクラスで必ず実装すべき抽象メソッド"""
        pass


class Triangle(Figure):
    # 三角形の面積を求めるためのget_areaメソッドを定義
    @override
    def get_area(self) -> float:
        return self.width * self.height / 2


class Rectangle(Figure):
    # 四角形の面積を求めるためのget_areaメソッドを定義
    @override
    def get_area(self) -> float:
        return self.width * self.height


if __name__ == '__main__':
    # 複数の図形インスタンスをリストにまとめる
    figs = [Triangle(10, 15), Rectangle(10, 15), Triangle(5, 1)]

    # for文で順に取り出して判定・処理
    for f in figs:
        if isinstance(f, Triangle):
            print(f"Triangle: width={f.width}, height={f.height}, area={f.get_area()}")
        elif isinstance(f, Rectangle):
            print(f"Rectangle: width={f.width}, height={f.height}, area={f.get_area()}")
        else:
            print(f"Unknown Figure: {type(f)}")
実行結果
Triangle: width=10, height=15, area=75.0
Rectangle: width=10, height=15, area=150.0
Triangle: width=5, height=1, area=2.5

for文以下の内容で、リストfigsから取り出した内容が、Figureクラスを継承しているかを確認している。Figure派生クラスであれば、get_areaメソッドを必ず実装しているはずなので、これを安全に取り出せる。

このように異なる種類のインスタンスが混在している場合でも、まとめて処理できるのがポリモーフィズムのメリットである。例えば、将来新しいDiamondクラスが追加されたとしても、呼び出しコードが影響を受けることはない。

ダックタイピング

ダックタイピング:「もしそれがアヒルのように歩き、アヒルのように鳴くなら、それはアヒルに違いない」
(If it walks like a duck and quacks like a duck, it must be a duck.)
→ オブジェクトの「型そのもの」ではなく、「持っているメソッドや属性」に基づいて扱うという考え方。

JavaやC++:オブジェクトがどのクラスを継承しているかを重要視し、型宣言を明示的に行い、型を確認してから処理する。
Python:クラス階層や警鐘を気にせずに、必要なメソッドが実装されていれば、そのオブジェクトを利用できる。
→ Pythonの方が柔軟である(が、同時に弱点でもある)

まずは簡単な例。

class Duck:
    def quack(self):
        print("Quack!")

class Person:
    def quack(self):
        print("I can also quack!")

def make_it_quack(obj):
    # 型は問わず「quackメソッドを持っていればOK」
    obj.quack()

d = Duck()
p = Person()

make_it_quack(d)  # Quack!
make_it_quack(p)  # I can also quack!

Figureクラスについて考えてみる。

from typing import override
from abc import ABC, abstractmethod

class Figure(ABC):
    # 幅と高さを準備
    def __init__(self, width: float, height: float) -> None:
        self.width = width
        self.height = height

    # 面積を取得する抽象メソッド
    @abstractmethod
    def get_area(self) -> float:
        """サブクラスで必ず実装すべき抽象メソッド"""
        pass


class Triangle(Figure):
    # 三角形の面積を求めるためのget_areaメソッドを定義
    @override
    def get_area(self) -> float:
        return self.width * self.height / 2


class Rectangle(Figure):
    # 四角形の面積を求めるためのget_areaメソッドを定義
    @override
    def get_area(self) -> float:
        return self.width * self.height

class Japan:
    def get_area(self) -> float:
        return 378000

# Figure型の値を受け取り、面積を算出/表示するための関数
def show_area(figure: Figure) -> None:
    print(f'{figure.__class__.__name__}の面積は{figure.get_area()}です。')


if __name__ == '__main__':
    show_area(Triangle(10, 15))    # Triangleの面積は75.0です。
    show_area(Rectangle(10, 15))   # Rectangleの面積は150です。
    show_area(Japan())             # Japanの面積は378000です。

実行はされるものの、show_area(Japan())では、型違反のアラートが発生する。
→ この例であれば、Japan型がFigure型を継承するように改めることで警告を抑制できるが、望ましくはない。JapanとFigureはis-aの関係にないからである。

これをダックタイピングの原理に従って書き直すと以下。

from typing import Protocol, runtime_checkable, override

# 「面積を計算できる」ものを表すインターフェイス的役割
@runtime_checkable
class Areable(Protocol):
    def get_area(self) -> float: ...
    

class Triangle:
    def __init__(self, width: float, height: float) -> None:
        self.width = width
        self.height = height

    @override
    def get_area(self) -> float:
        return self.width * self.height / 2


class Rectangle:
    def __init__(self, width: float, height: float) -> None:
        self.width = width
        self.height = height

    @override
    def get_area(self) -> float:
        return self.width * self.height


class Japan:
    def get_area(self) -> float:
        return 378000


# 「Areableなオブジェクト」なら受け入れる
def show_area(figure: Areable) -> None:
    print(f'{figure.__class__.__name__}の面積は{figure.get_area()}です。')


if __name__ == '__main__':
    show_area(Triangle(10, 15))    # Triangleの面積は75.0です。
    show_area(Rectangle(10, 15))   # Rectangleの面積は150です。
    show_area(Japan())             # Japanの面積は378000です。

アヒルのように歩く(鳴く)ことを表すのが、Protocolの役割。Protocol型を継承して、空のメソッドを定義すればよい。これで、get_areaメソッドを持つAreableプロトコルを定義したことになる。
あとは、show_area関数の引数型をAreableに変更すればよい。Japan/Triangle/Rectangleに特別な関連付けがない点も注目。

このように、ダックタイピングを用いれば、Pyhtonの動的型付けの性質にもなじみやすく、シンプルにポリモーフィズムを実装できる。
厳密な型体系を表現したいなら、継承を用いるべきであるし、メソッドの有無のミニ関心がある場合は、シンプルなダックタイピングが好まれる。

参考文献
[1] 独習Python (2020, 山田祥寛, 翔泳社)
[2] Pythonクイックリファレンス 第4版(2024, Alex, O’Reilly Japan)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?