abcで抽象クラスが使えるらしいけど…
他の言語をやっていた人だと使いたくなるポリモーフィズム。Pythonでもやってみましょう。
こんにちは、ライターのフクロウです。Pythonのインストラクターをやっています。
この記事では一つのインターフェイスに対して複数の実装をするポリモーフィズムについて学んでいきます。綺麗なコードを書きたい、大きなプログラムを効率的に開発したいという方には是非知っておいて欲しい概念です。
この記事はこんな人のために書きました。
- ポリモーフィズムという言葉の意味が知りたい
- Pythonで抽象基底クラスが使いたい
- Pythonでポリモーフィズムを試してみたい
本記事を読む前に、Pythonがどんなプログラミング言語なのかをおさらいしておきたい人は次の記事を参考にしてください。
→ Pythonとは?特徴やできること、活用例をわかりやすく簡単に解説
なお、その他のPythonの記事についてはこちらにまとめています。
ポリモーフィズムとは
ポリモーフィズムとは何でしょうか。Wikipediaによると以下のように解説されています。
ポリモーフィズム(英: Polymorphism)とは、プログラミング言語の型システムの性質を表すもので、プログラミング言語の各要素(定数、変数、式、オブジェクト、関数、メソッドなど)についてそれらが複数の型に属することを許すという性質を指す。
ポリモーフィズムはいくつかの種類に分けられて、例えば以下のものが知られています。
- アドホックポリモーフィズム
- パラメータポリモーフィズム
- サブタイプポリモーフィズム
ポリモーフィズム自体についての解説はニコニコ大百科の記事が分かりやすいと思います。
さて、Pythonはオブジェクト指向言語で、abcというモジュールを使うことでポリモーフィズムの恩恵を受けるのがこの記事の目的です。
そしてここでのポリモーフィズムとは、同じインターフェイスで異なる型のオブジェクト向けに動作を提供するものです。例えば同じ+演算子でもポリモーフィズムに則って実装されているため、int型の足し算もsrt型の足し算も出来てしまいます。
>>> 1+2 3 >>> "あ"+"い" 'あい'
add_int、add_strのように別の関数を用意しないでも同様の事ができています。たぶんこっちのほうが分かりやすいですよね。
クラスでも、異なるクラスで同じ名前のメソッドを用意することで実現できます。
これができると、様々なクラスを定義したとしてもどのクラスのインスタンスにも同じ名前のメソッドがあることになります。結果プログラムの見通しが良くなって大規模な開発がしやすくなるはずです。この記事ではこれを扱います。
抽象基底クラス
abcモジュールで抽象基底クラスを作る
Pythonにはabcという標準ライブラリが用意されています。これを使うことでポリモーフィズムを強制することができます。
使い方は簡単で、ひな形としてインターフェイスの模範にしたいクラスを以下のように用意します。
from abc import ABCMeta, abstractmethod, ABC
class AbstractAgent(metaclass=ABCMeta):
    
    @abstractmethod
    def __init__(self, hp=100, mp=100):
        self.hp = hp
        self.mp = mp
    
    @abstractmethod
    def __str__(self):
        raise NotImplemented()
    
    @abstractmethod
    def attack(self, target):
        target.hp -= 10
        print(f"{target}へ体当たり攻撃!")
雛形のクラス(抽象基底クラスと呼びます)では、親クラスを書いていた括弧の中に「metaclass=ABCMeta」と記述します。これで抽象基底クラスとして定義されました。
また、どのクラスでも__init__と__str__、attackの3つのメソッドを用意させたいとしましょう。その場合それぞれのメソッドに「@abstractmethod」というデコレータを用意します。
このデコレータがついたメソッドは、この抽象基底クラスを継承したサブクラスでも必ず用意しなければ行けないメソッドになります。
これを抽象メソッドといいますが、abcモジュールではこのメソッドの中にちゃんと処理も書くことができます。
ABCMetaを使ったクラスの注意点
抽象基底クラスからインスタンスを作ることは出来ません。例えば以下のようにしてインスタンスを作ろうとすると、エラーが発生します。
>>> p1 = AbstractAgent() --------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-71-7f498ecb08b1> in <module> ----> 1 p1 = AbstractAgent() TypeError: Can't instantiate abstract class AbstractAgent with abstract methods __init__, __str__, attack
ABCMetaがつかない普通の親クラスであればこの操作は可能ですが、このように定義したクラスは実体を持てないことに注意しましょう。
抽象基底クラスを継承したサブクラス
サブクラスを作ってみる
それでは先程のクラスを継承したサブクラスを作ってみましょう。
ここではAbstractAgentという抽象基底クラスを継承した、SpellCaster、SwordManというクラスを作ります。(ゲームを開発してると思ってください)
class SpellCaster(AbstractAgent):
    def __init__(self, name, hp=100, mp=200):
        super().__init__(hp=hp,mp=mp)
        self.name = "魔法使い「"+str(name)+"」"
        
    def __str__(self):
        return f"■name: {self.name}n■hp:{self.hp}n■{self.mp}"
    
    def attack(self, target):
        target.hp -= 20
        self.mp -= 10
        print(f"{self.name}が{target.name}へ魔法攻撃!")
        
class SwordMan(AbstractAgent):
    def __init__(self, name, hp=200, mp=100):
        super().__init__(hp=hp, mp=mp)
        self.name = "剣士「"+str(name)+"」"
        
    def __str__(self):
        super().__str__()
    
    def attack(self, target):
        target.hp -=15
        self.hp -= 5
        print(f"{self.name}が{target.name}へ剣で攻撃!")
        
p1 = SpellCaster("ジャック")
p2 = SwordMan("ジョン")
p1.attack(p2)
[出力結果の例]
魔法使い「ジャック」が剣士「ジョン」へ魔法攻撃!
この2つのクラスは抽象基底クラスと同様に3つのメソッドを持っていますね。
それぞれがそのクラス向けの機能を実装していますが、インターフェイスは同じです。同様の名前でアクセスでき、仮に定義を忘れても次のセクションで紹介するようにエラーで知らせてくれます。
また、これはabcを使わない場合でも有効ですが、親クラスのメソッドに「raise NotImplemented()」を書いておくことで、親クラスのメソッドをそのまま使った場合にエラーを出すことが出来ます。
>>> print(p2)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-80-ad0749cbb249> in <module>
----> 1 print(p2)
<ipython-input-78-43416f20f217> in __str__(self)
     18 
     19     def __str__(self):
---> 20         super().__str__()
     21 
     22     def attack(self, target):
<ipython-input-70-438fdc87a1fa> in __str__(self)
      8     @abstractmethod
      9     def __str__(self):
---> 10         raise NotImplemented()
     11 
     12     @abstractmethod
TypeError: 'NotImplementedType' object is not callable
大規模なプログラムを書くときに有効な方法です。ぜひ使ってみてください。
サブクラスの注意点
抽象基底クラスを継承したクラスの定義は普通と同じでした。ですが、必要なメソッドが足りなければエラーがでるので注意して下さい。
class SpellCaster(AbstractAgent):
    pass
        
p1 = SpellCaster()
[エラー例]
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-71-7f498ecb08b1> in <module> ----> 1 p1 = AbstractAgent() TypeError: Can't instantiate abstract class AbstractAgent with abstract methods __init__, __str__, attack
このエラーはクラスを定義しただけでは発生しません。
クラスからインスタンスを作る段階でエラーが発生して足りないインターフェイスを教えてくれます。
また、たとえ途中までメソッドを定義していたとしても、一つでも足りなければ先ほどと同様にエラーが発生します。
class Archer(AbstractAgent):
    def __init__(self, name, hp=150, mp=150):
        super().__init__(hp=hp, mp=mp)
        self.name = "弓兵「"+str(name)+"」"
    
    def attack(self, target):
        target.hp -=15
        self.hp -= 5
        print(f"{self.name}が{target.name}で弓矢で攻撃!")
        
p1 = Archer("花子")
[エラー例]
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-76-19d1914142c6> in <module>
      9         print(f"{self.name}が{target.name}で弓矢で攻撃!")
     10 
---> 11 p3 = Archer("花子")
TypeError: Can't instantiate abstract class Archer with abstract methods __str__
この手法のメリット
上のようにabcモジュールを使って抽象基底クラスを作ることで、複数のクラスで同等のインターフェイスを用意することができます(出来ていなければエラーが出るため)。
普通の親クラスであれば、実装を忘れたメソッドがあっても親クラスのメソッドが呼ばれるだけです。この方法だとインタプリタがしっかり教えてくれるので、バグが減るはずです。
まとめ
いかがだったでしょうか。abcモジュールを使ったクラスの作成は今までやったことがないという方も多かったと思います。
今回は抽象基底クラスを使ったポリモーフィズムを紹介しましたが、プログラミングを勉強して行けばこれ以外にも様々な場面でこの言葉に遭遇するでしょう。
文章で読むと難しいことでも、実装例を見てみると理解できることは多いはずです。是非手を動かしてプログラミングの勉強してください。
 
  






 
        