オブジェクト指向のポリモーフィズムとは

先にクラスの記事を読んでおくことをお勧めします。 infrastructure-tech.hatenablog.com

ポリモーフィズムとは

プログラミング言語の各要素(定数、変数、式、オブジェクト、関数、メソッドなど)についてそれらが複数の型に属することを許すという性質を指す。ポリモルフィズム多態性、多相性、多様性とも呼ばれる。

難しいですね。
ざっくり言うとサブルーチンの真逆の動きをすることです。
サブルーチンの場合、サブルーチン(呼び出される側)は複数のメインルーチン(呼び出す側)に共通でした。
ポリモーフィズムの場合、メインルーチン(呼び出す側)は複数のサブルーチン(呼び出される側に)に共通です。 それぞれの共通ルーチンは、対応するルーチンが増えても修正する必要がありません

ポリモーフィズムについて解説しているサイトでは、サンプルコードに抽象クラスや継承を利用していることが多いですが、継承の利用は必須ではないと思います*1 ただ、ポリモーフィズムを実現には継承が効率的なため、広く利用されているのかと思います。

メインルーチン サブルーチン
サブルーチン 複数 共通
ポリモーフィズム 共通 複数

良く上げられる例が動物の例えです。 クラスの記事でも例えましたが、犬や猫は動物クラスにいます。
動物に対して「鳴け」と指示すると、犬ならば「ワン」猫ならば「にゃー」と鳴き、対象の動物によって動作が異なります。

ただこの例えは分かりやすい反面、クラスと同じように混乱を招く要因だと思います。
なぜなら私たちは動物に対して「鳴け」と指示することができないからです。
犬や猫など(具現化された)特定の動物に対して訓練し指示すること はできますが、「動物」のように抽象化された概念に指示することはありません。

そこで今回は実際に利用しているstr関数で考えてみましょう。
何気なく使っているstr関数ですが、よく考えると様々な型であってもString型に変換してくれています。

num_i = 100,
num_f = 10.5
array =['A','B','C']
handan = True

print(type(num_i),type(num_f),type(array),type(handan))
#変換前
#<class 'tuple'> <class 'float'> <class 'list'> <class 'bool'>
a =str(num_i)
b =str(num_f)
c =str(array)
d =str(handan)

print(type(a),type(b),type(c),type(d))
#変換後
#<class 'str'> <class 'str'> <class 'str'> <class 'str'>

strというメインルーチンは共通で、各サブルーチンの処理の中身を意識する必要がありません。
ポリモーフィズムの動作イメージが付いたでしょうか?

コード例(抽象クラス・継承無し)

実際にプログラムしながら理解を深めていきます。
まず継承を使わないバージョンのコードです。

class aircon():
    def on(self):
        print('【エアコン】<ON')
    def off(self):
        print('【エアコン】<OFF')

class fan():
    def on(self):
        print('【扇風機】<ON')
    def off(self):
        print('【扇風機】<OFF') 

machine = aircon()
machine.on()
machine.off()
#【エアコン】<ON
#【エアコン】<OFF

machine = fan()
machine.on()
machine.off()
#【扇風機】<ON
#【扇風機】<OFF

machine.on()machine.off()と同じon/offメソッドを実行していますが、machineに対して与えたオブジェクトによって出力結果が異なることが分かります。
machine = aircon()の時はエアコンが操作され、machine = fan()の時は扇風機が操作されています。
ただしmachineでは何のオブジェクトが入っているかを気にする必要がありません これでも動きとしてはポリモーフィズムと言えると思います。
が、オブジェクト指向で散々言われている「抽象化」「概念」は?と思うと思いますので、次に継承を用いた例を考えてみます。

コード例(抽象クラス・継承有り)

続いて抽象クラスを継承したバージョンです。
何やらimportして、新しくsignalクラスが作られていることが分かります。

ちなみにfrom abc import ABCMeta, abstractmethodは「from モジュール名 import クラス名」の意味があります。
import abcの場合abc.@abstractmethodとモジュール名も記載する必要がありますが、fromを使うことでクラス名を直接使うことができます。

from abc import ABCMeta, abstractmethod #抽象基底クラスをインポート
class signal(metaclass = ABCMeta): #signalクラスを作成
    @abstractmethod #各メソッドにデコレータ@abstractmethodを付与
    def on(self): #メソッド
        pass
    @abstractmethod
    def off(self):
        pass

class aircon(signal):   #signalクラスを継承
    def on(self):
        print('【エアコン】<ON')
    def off(self):
        print('【エアコン】<OFF')

class fan(signal):
    def on(self):
        print('【扇風機】<ON')
    def off(self):
        print('【扇風機】<OFF')  

machine = aircon()
machine.on()
machine.off()
#【エアコン】<ON
#【エアコン】<OFF

machine = fan()
machine.on()
machine.off()
#【扇風機】<ON
#【扇風機】<OFF

まずfrom abc import ABCMeta, abstractmethodで抽象基底クラスであるabcから、「ABCMeta」と「abstractmethod」を呼び出しています。 なおPython3.4以降の機能のようです。 詳細は以下のサイトにありますが、「ABC」を継承することでも同じ動きが実現できます。

from abc import ABC
class MyABC(ABC):
    pass

docs.python.org

動きを解説するまえに、今出てきた単語の意味を解説します。

abc.ABCMeta

抽象基底クラス(ABC)を定義するためのメタクラス

抽象クラスとは

抽象クラスは、クラスのメタクラスにabc.ABCMetaを設定することで実装可能です。
動物の例えは理解しづらいと思うため、今回はリモコンを例にして考えていきます。

「エアコンのリモコン」と「扇風機のリモコン」は別々の動きをしますが、2つのリモコンには共通点もあります。
それは「ON」や「OFF」の機能があるという点です。
そこで抽象化をし、「リモコン」という概念を作り出します。
この段階で以下の状態になります。

信号を出すリモコン > エアコンのリモコン > ON/OFF機能、冷房、暖房
          > 扇風機のリモコン  > ON/OFF機能、風量、首振り
↓抽象化
信号を出すリモコン > ON/OFF機能

この抽象化されたリモコンが、抽象クラスであるsignalにあたります。

class signal(metaclass = ABCMeta):
    @abstractmethod
    def on(self):
        pass
    @abstractmethod
    def off(self):
        pass

メタクラスとは

メタクラスとはインスタンスがクラスとなるクラスのことである。通常のクラスがそのインスタンスの振る舞いを定義するように、メタクラスはそのインスタンスであるクラスを、そして更にそのクラスのインスタンスの振る舞いを定義する。

難解な文章ですね。順番に理解していきましょう。

メタクラスとはインスタンスがクラスとなるクラスのことである。

クラスからインスタンスを生成するように、メタクラスからクラスを生成できます。

②通常のクラスがそのインスタンスの振る舞いを定義するように

通常、クラスはインスタンスの振る舞いを定義しています。

メタクラスはそのインスタンスであるクラスを

同じようにメタクラスはクラスの振る舞いと、

④そして更にそのクラスのインスタンスの振る舞いを定義する。

クラスのインスタンスの振る舞いも定義しています。

ざっくり言うとメタクラス*2>クラス*3>インスタンスという流れになっています。

@abstractmethod

抽象メソッドを示すデコレータです。

デコレータとは

本題とは少し離れますが、ここでは簡単なデコレータの例を使って解説します。
デコレータを使うことで、関数の前後や戻りを修飾(Decorator)することができます。
デコレータは糖衣構文(syntax sugar)と呼ばれ、初心者には難しい構文も簡単に見せることができます。
*args, **kwargsは、デコレートする関数の引数を挿しています。

def decorator(func):  #デコレータの定義
    def cookie(*args, **kwargs):
        print('オ')
        func(*args, **kwargs)
        print('オ!')
    return cookie

@decorator  #デコレータ
def write():
    print('レ')

write()
#オ
#レ
#オ!

あら不思議「オレオ!」と出力されました。
なお@decoratorを付けないと、「レ」しか出力されません。

うーん横並びのほうが綺麗だな・・・と思った方は戻り値をデコレートしてみましょう。 decorator()とwrite()を変更しました。

def decorator(func):  #デコレータの定義
    def cookie(*args, **kwargs):
        result ="オ" + func(*args, **kwargs)+"オ!"  #文字列結合
        return result
    return cookie

@decorator  #デコレータ
def write():
    return 'レ'  #returnに変更
print(write())
#オレオ!

これで横並びに出力できました!
func(*args, **kwargs)の戻り値になっている「レ」が、デコレータによって「オ」と「オ!」が結合されています。
このように自由に関数を修飾できるのがデコレータです。
なお複数付けた場合はどうなるでしょうか。

def decorator(func):  #デコレータの定義
    def cookie(*args, **kwargs):
        print('オ')
        func(*args, **kwargs)
        print('オ!')
    return cookie
def decorator2(func):  #デコレータの定義
    def cream(*args, **kwargs):
        print('クリーム')
        func(*args, **kwargs)
        print('クリーム')
    return cream

@decorator  #デコレータ
@decorator2
def write():
    print('レ')

write()
#オ
#クリーム
#レ
#クリーム
#オ!

プログラムの上から処理されるイメージがあるとおもいますが、デコレータの場合関数に近いほうから処理されます。 「レ」をdecorator2でデコレートすると「クリーム レ クリーム」
「クリーム レ クリーム」をdecoratorでデコレートすると「オ クリーム レ クリーム オ!」になるという流れです。

@abstractmethodの役割

改めて@abstractmethodについて解説します。
このデコレータをメソッドにつけることで、明示的に抽象メソッドであることを示し、インスタンスでの実装を強制することができます。 先ほどのコードを元に解説していきます。

signalクラスのonメソッドに@abstractmethodが付与されています。 そしてsignalクラスを継承したairconクラスにも同じ名前のonメソッドがあります。

class signal(metaclass = ABCMeta): #signalクラスを作成
    @abstractmethod #各メソッドにデコレータ@abstractmethodを付与
    def on(self): #メソッド
        pass
~中略~
class aircon(signal):   #signalクラスを継承
    def on(self):
        print('【エアコン】<ON')

クラスのメソッド名誤り

airconクラスのメソッド名を誤入力し、「xxx」というメソッド名で作成したらどうなるでしょうか。

from abc import ABCMeta, abstractmethod
class signal(metaclass = ABCMeta):
    @abstractmethod
    def on(self):
        pass
~中略~
class aircon(signal):
    def xxx(self):
        print('【エアコン】<ON')
~中略~
machine = aircon()
#TypeError: Can't instantiate abstract class aircon with abstract methods on

aircon()をインスタンス化した段階で「 抽象メソッド "on "の抽象クラスairconをインスタンス化できない」とエラーが出ました。
これによりクラスのメソッド名が誤っていた場合に気づくことができます。

@abstractmethodを付与しない

ではsignal抽象クラスのonメソッドに「@abstractmethod」を付与しなかったらどうなるでしょうか。

from abc import ABCMeta, abstractmethod
class signal(metaclass = ABCMeta):
    def on(self):
        pass
~中略~
class aircon(signal): 
    def xxx(self):
        print('【エアコン】<ON')
~中略~
machine = aircon()
machine.xxx()
#【エアコン】<ON

問題無くインスタンス化し実行されました。
signal抽象クラスで「@abstractmethod」が付与されていないメソッドの実装は、クラスへ強制されません。
つまり作成するクラスによっては不要となるメソッドは「@abstractmethod」を付与しません

また全てのクラスで共通のメソッドにも付与する必要がありません
クラスへ定義しなくてもstopメソッドが実行できています。

from abc import ABCMeta, abstractmethod
class signal(metaclass = ABCMeta):
    def stop(self):  #共通メソッド
        print('【共通】<STOP')
class aircon(signal):
    def on(self):
        print('【エアコン】<ON')
    def off(self):
        print('【エアコン】<OFF')
machine = aircon()
machine.stop()
#【共通】<STOP

解説

改めて先ほどのコードを見てみると動きが理解できるようになったでしょうか。

#抽象基底クラスをインポート
from abc import ABCMeta, abstractmethod 

#signal抽象クラスを作成
class signal(metaclass = ABCMeta):  metaクラスをABCMetaに指定

    #各メソッドにデコレータ@abstractmethodを付与
    @abstractmethod
   #共通のonメソッド
    def on(self):
        pass

    @abstractmethod
    #共通のoffメソッド
    def off(self):
        pass

#signalクラスを継承したairconクラス
class aircon(signal):   
    #airconクラスのonメソッド
    def on(self):
        #onメソッドの実行内容
        print('【エアコン】<ON')
    #airconクラスのoffメソッド
    def off(self):
        #onメソッドの実行内容
        print('【エアコン】<OFF')

class fan(signal):
    def on(self):
        print('【扇風機】<ON')
    def off(self):
        print('【扇風機】<OFF')  

#airconクラスをインスタンス化
machine = aircon()

#machineオブジェクトのon/offメソッドの実行
machine.on()
machine.off()

#【エアコン】<ON
#【エアコン】<OFF

#fanクラスをインスタンス化
machine = fan()

#machineオブジェクトのon/offメソッドの実行
machine.on()
machine.off()
#【扇風機】<ON
#【扇風機】<OFF

抽象クラスのabcをインポートします。

#抽象基底クラスをインポート
from abc import ABCMeta, abstractmethod 

抽象クラスのsignalクラスを作成 metaclass = ABCMetaメタクラスを指定

#signal抽象クラスを作成
class signal(metaclass = ABCMeta): 

継承したいクラス(airconやfan)に作成するメソッドを作成します。 @abstractmethodを付けることで、継承先クラスでのメソッドの作成を強制することができます。
処理内容が共通するメソッドや不要なメソッドは@abstractmethodを付ける必要がありません。

    #各メソッドにデコレータ@abstractmethodを付与
    @abstractmethod
   #共通のonメソッド
    def on(self):
        pass
    @abstractmethod
    #共通のoffメソッド
    def off(self):
        pass

class aircon(signal): でsignal抽象クラスを継承します。
そしてsignalクラスで作成した各メソッドの処理内容を作成します。

#signalクラスを継承したairconクラス
class aircon(signal):   
    #airconクラスのonメソッド
    def on(self):
        #onメソッドの実行内容
        print('【エアコン】<ON')
    #airconクラスのoffメソッド
    def off(self):
        #onメソッドの実行内容
        print('【エアコン】<OFF')

class fan(signal):
    def on(self):
        print('【扇風機】<ON')
    def off(self):
        print('【扇風機】<OFF')  

各クラスをインスタンス化し、machineオブジェクトを作成します。

#airconクラスをインスタンス化
machine = aircon()
#fanクラスをインスタンス化
machine = fan()

以降はメインプログラムです。
machineオブジェクトのon/offメソッドを実行します。
同じメソッド名を実行しても、machineに入れたオブジェクトによって結果が異なることが分かります。

#machineオブジェクトのon/offメソッドの実行
machine.on()
machine.off()

#【エアコン】<ON
#【エアコン】<OFF

#machineオブジェクトのon/offメソッドの実行
machine.on()
machine.off()
#【扇風機】<ON
#【扇風機】<OFF

「違うインスタンスを作成しているのだから、当たり前じゃ?」「これがメインルーチン?」と思う方は以下のプログラムを実行してみてください。

from abc import ABCMeta, abstractmethod
import abc

class signal(abc.ABC):
    @abstractmethod
    def on(self):
        pass
    @abstractmethod
    def off(self):
        pass
    def stop(self):  #共通メソッド
        print('【共通】<STOP')

class aircon(signal):
    def on(self):
        print('【エアコン】<ON')
    def off(self):
        print('【エアコン】<OFF')

class fan(signal):
    def on(self):
        print('【扇風機】<ON')
    def off(self):
        print('【扇風機】<OFF')  

class remocon():
    def on(self,machine):
        machine.on()
    def off(self,machine):
        machine.off()

remocon = remocon()
machine1 = aircon()
machine2 = fan()

remocon.on(machine1)
remocon.off(machine1)
remocon.on(machine2)
remocon.off(machine2)

#【エアコン】<ON
#【エアコン】<OFF
#【扇風機】<ON
#【扇風機】<OFF

remocon.onにairconオブジェクトを渡すと【エアコン】<ONが出力され、fanオブジェクトを渡すと 【扇風機】<ONが出力されます。
ここでエアコンや扇風機以外の機能を実装したい場合、サブルーチン側に機能を追加することで実現できます。
そしてメインルーチンのremoconは変更する必要がありません。
このように同じメソッドを実行したときに、与えたオブジェクトによって異なる振る舞いをすることがポリモーフィズムです。

まとめ

与えたオブジェクトによって異なる振る舞いをすること
複数のサブルーチンに共通のメインルーチン
共通のメインルーチンは修正する必要がない 抽象化(abc)を用いることでもポリモーフィズムを実現できる
@abstractmethodで抽象メソッドを指定する

*1:誤っていたらご指摘ください。

*2:クラスのクラスでもある

*3:メタクラスインスタンスでもある