Python Tips: Python でインタフェースを使いたい

Python でインタフェースの機能を使う方法をご紹介します。

・・・といっても、 Python 3.6 の時点で Python には言語機能としてのインタフェースは存在しません。具体的にいうと、「継承先に特定のインタフェースの実装を強制できるような仕組み」が Python にはありません。

しかし、標準ライブラリの abc を使うと次の 2 つのことが実現できます。

  • a. 複数のクラスを抽象クラスでまとめる
  • b. 継承先クラスのインスタンス生成時にメソッドの存在チェックをかける

このことをもって、「 Python でも他の言語でいうところの『インタフェース』の機能をある程度は利用できる 」と考えてもよいのではないかな、と個人的には思います。

今回はこの Python におけるインタフェース的な機能を abc で実現する方法をご紹介します。上の a b を順に見ていきましょう。

a. 複数のクラスを抽象クラスでまとめる

最初に、複数のクラスを抽象クラスでまとめる方法を見てみます。

複数の異なるクラスを抽象クラスでまとめることで、それらのクラスのオブジェクトに対して isinstance() が True を返すようなチェック機構を作ることができます。

具体的なコードを見てみましょう。

# coding: utf-8

'''デジタルコンテンツインタフェースを作ってテストする
'''

from abc import ABC

class Movie:
    '''映画
    '''
    pass

class Book:
    '''書籍
    '''
    pass

class DigitalContentInterface(ABC):
    '''デジタルコンテンツのインタフェース
    '''
    pass

# 映画と書籍を DigitalContentInterface の仮想サブクラスとして登録する
DigitalContentInterface.register(Movie)
DigitalContentInterface.register(Book)

def is_digital_content(object):
    '''オブジェクトがデジタルコンテンツかどうかをチェック
    '''
    return isinstance(object, DigitalContentInterface)

# Movie のインスタンスは DigitalContentInterface のインスタンスと認識される
m1 = Movie()
print(is_digital_content(m1))  # => True

# Book のインスタンスも DigitalContentInterface のインスタンスと認識される
b1 = Book()
print(is_digital_content(b1))  # => True

# 登録されていないクラスのインスタンスでは当然 False が返る
d1 = {}
print(is_digital_content(d1))  # => False

ここでは abc.ABC というクラスを継承する形で DigitalContentInterface というインタフェース用のクラスを作成しました。

続いて、クラスメソッド register() を使って、 MovieBook という 2 つのクラスを DigitalContentInterface の仮想サブクラスとして登録しています。

その後に isinstance() でチェックをかけると、 MovieBook のインスタンスは DigitalContentInterface のインスタンスと認識されることが確認できます。

おもしろいですね。

register() メソッドが便利なのは、組み込みのクラスを含む定義済みのクラスも「独自に作ったインタフェース」の仮想サブクラスとして登録できるところです。

# リストは本来(当然) DigitalContentInterface のインスタンスではない
l1 = []
print(is_digital_content(l1))  # => False

# リストクラスを DigitalContentInterface の仮想サブクラスとして登録すると・・・
DigitalContentInterface.register(list)

# リストのインスタンスも DigitalContentInterface のインスタンスと認識されるようになる
print(is_digital_content(l1))  # => True

ちなみに、この、特定のクラスを仮想サブクラスとして登録する register() メソッドを使った方法とは別に、クラスメソッド __subclasshook__() を使った方法も用意されています。

公式のサンプルを少し縮めたサンプルを見てみましょう。

class MyIterable(ABC):

    @classmethod
    def __subclasshook__(cls, C):
        if cls is MyIterable:
            if any("__iter__" in B.__dict__ for B in C.__mro__):
                return True
        return NotImplemented

ここでは、 __subclasshook__() メソッドを実装することで「 __iter__() メソッドを実装しているクラスは MyIterable のサブクラスとみなす」というロジックが実現されています。 __subclasshook__() を使えば、このようにより柔軟なロジックで仮想サブクラスを設定することができます(例えば、インタフェースの定義時には具体的な仮想サブクラスがわからない状態でも仮想サブクラスを登録することができます)。

ちなみに、この abc.ABC はメタクラス abc.ABCMeta をよりシンプルに扱えるクラスとして Python 3.4 で導入されました。

a については以上です。続いて b の方を説明していきます。

b. 継承先クラスのインスタンス生成時にメソッドの存在チェックをかける

こちらは、よりインタフェースっぽい感じのふるまいが実現できる機能です。

こちらも先にサンプルコードを見てみましょう。

# coding: utf-8

'''デジタルコンテンツインタフェースでメソッドの実装の強制機能をテストする
'''

from abc import ABC
from abc import abstractmethod

class DigitalContentInterface(ABC):
    '''デジタルコンテンツのインタフェース
    '''

    def __init__(self, title):
        self.title = title

    @abstractmethod
    def format_title(self):
        pass

class Movie(DigitalContentInterface):

    def format_title(self):
        return 'Movie: {}'.format(self.title)

class Book(DigitalContentInterface):
    pass

# Movie のインスタンスは問題なく生成できる
m1 = Movie('From Dusk Till Dawn')
print(m1.format_title())
# => Movie: From Dusk Till Dawn
print(isinstance(m1, DigitalContentInterface))
# => True

# Book のインスタンスは
# Book が format_title() メソッドを定義していないので作成できない
b1 = Book('The Martian')
# TypeError: Can't instantiate abstract class Book with abstract methods format_title

以下、コード内の処理の流れを説明します。

まず最初に、 a と同じような形で abc.ABC クラスを継承した DigitalContentInterface というクラスを定義しています。この中で abc.abstractmethod というデコレータ用の関数を使って format_title() というメソッドをアブストラクトメソッドとして登録しています。

つづいて、 a の register() を使った形ではなく、通常の継承のシンタックスを使って DigitalContentInterface を継承したクラス MovieBook を定義しています。

実際にインスタンスを生成しようとすると、 Movie の方は問題なく生成できますが、 Book の方はアブストラクトメソッド format_title() をきちんと実装していないということで例外が上がって生成できません。

・・・というように、「インスタンス生成時にメソッドの存在チェックをかける」ことができていることが確認できます。

こちらもおもしろいですね。うまく活用できると便利そうです。

ただし、 b の方は使う際に押さえておくべき注意点がいくつかあります。

  • register() を使った方法で登録した仮想サブクラスに対してはチェックが走らない
  • @abstractmethod で登録されたメソッド自身が `pass` 以外の実装を持つことができて、サブクラスから super() で呼び出すことができる

ここで取り上げたのはインスタンスメソッドを使ったパターンだけでしたが、クラスメソッド、スタティックメソッド、プロパティに対しても同様のチェックを行うこともできます。その場合はデコレータの記述順などにも厳密なルールがあるため、興味のある方は利用の前に公式のドキュメントなどでしっかり確かめてから使うようにするとよいかと思います。

以上です。

おもしろいですねー。

参考