PythonのABC - 抽象クラスとダック・タイピング

  • 64
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

この記事はPython Advent Calendar 2015の8日目の記事です。


普段はGoを書くのがほとんどで、Pythonは正直滅多に書かないです。数理学でそろそろ本腰いれて使用していこうかと思っているので、今回PythonのAdventCalendarに飛び込んでみました。

さて、Goではinterfaceを定義して抽象化されたパターンで実装していくことが大規模に開発していくには重要になります。これは別にGoに限った話ではなくオブジェクトは抽象化され、節度をを守って使用されるのが可読性の高く、また、依存性の低い素晴らしいコードへとなっていきます。

そんな中、Pythonで抽象化をするにはどういう手法があるのか調べてみました。言語仕様には存在せずABC (Abstract Base Class) という名前のモジュールとして提供されているものを使用します。

ABC - Abstract Base Classes モジュール

Pythonでは抽象クラスをABC (Abstract Base Class - 抽象基底クラス)モジュールを使用して実装することができます。
抽象基底クラスはABCMetaというメタクラスで定義することが出来、定義した抽象基底クラスをスーパークラスとしてサブクラスを定義することが可能です。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from abc import ABCMeta, abstractmethod

# 抽象クラス
class Animal(metaclass=ABCMeta):
    @abstractmethod
    def sound(self):
        pass

# 抽象クラスを継承
class Cat(Animal):
    def sound(self):
        print("Meow")

if __name__ == "__main__":
    assert issubclass(Cat().__class__, Animal)
    assert isinstance(Cat(), Animal)

Animalという抽象クラスを定義し、それを継承したCatクラスを実装しています。
継承しているため、issubclass, isinstanceが通るのは当たり前な感じですね。

継承しているCatクラスの方で抽象メソッド (@abstractmethodについては後述) のsoundを実装していない場合は下記のようにruntimeエラーとなります。(※インスタンス生成時)

class Cat(Animal):
    pass

# TypeError: Can't instantiate abstract class Cat with abstract methods sound

registerメソッドで仮想的サブクラス登録

一方、サブクラスを定義するのではなく、無関係のクラスを抽象クラスのように振る舞えるよう登録することも可能です。これを仮想的サブクラスというようです。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from abc import ABCMeta, abstractmethod

class Animal(metaclass=ABCMeta):
    @abstractmethod
    def sound(self):
        pass

# 抽象クラスを継承しないが、 `sound`メソッドを実装
class Dog():
    def sound(self):
        print("Bow")

# 抽象クラスのAnimalへDogを登録
Animal.register(Dog)

if __name__ == "__main__":
    assert issubclass(Dog().__class__, Animal)
    assert isinstance(Dog(), Animal)

仮想的サブクラスへの登録をすればそのクラスは抽象クラスのものとなりますが、抽象メソッドが実装されていない場合は下記のようにruntimeエラーとなります。(※メソッド呼び出し時)

class Dog():
    pass

# AttributeError: 'Dog' object has no attribute 'sound'

抽象メソッドデコレータ

@abstractmethod

抽象メソッドを示すデコレータです。抽象メソッドですが、デコレータを指定したメソッドに処理を記述し、サブクラスから呼び出すことも可能です。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from abc import ABCMeta, abstractmethod

class Animal(metaclass=ABCMeta):
    @abstractmethod
    def sound(self):
        print("Hello")

# 抽象クラスを継承
class Cat(Animal):
    def sound(self):
        # 継承元のsoundを呼び出す
        super(Cat, self).sound()
        print("Meow")

if __name__ == "__main__":
    print(Cat().sound())

super(Cat, self).sound()で継承元の抽象メソッドを呼び出すことができます。Javaとは少し違う印象ですね。

@abstractclassmethod (version 3.2)

抽象クラスメソッドのデコレータですが、バージョン3.3からは下記のように@classmethodで記述する。

class Animal(metaclass=ABCMeta):
    @classmethod
    @abstractmethod
    def sound_classmethod(self):
        pass

@abstractstaticmethod (version 3.2)

抽象静的メソッドのデコレータですが、バージョン3.3からは下記のように@staticmethodで記述する。

class Animal(metaclass=ABCMeta):
    @staticmethod
    @abstractmethod
    def sound_staticmethod(self):
        pass

ダック・タイピング

抽象クラスについては上記で十分ですが、ここまでやったらダック・タイピングも調べておきたいと思ったので実装してみます。

ダック・タイピングとは?

"If it walks like a duck and quacks like a duck, it must be a duck." - 「アヒルのように歩き、鳴けば、それはアヒルだ。」

ちょっと名言っぽいですよね。

これを簡素化して「鳴けばそれは動物だ。」としましょう。そして無理やりプログラミングに落とし込んでやると「あるオブジェクトに「鳴く」というメソッドを実装してやれば、その具象クラスは「動物」である」となります。初見の方にはわかりにくいですね。

実装

頭で考えるより見た方が早い。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from abc import ABCMeta, abstractmethod

class Animal(metaclass=ABCMeta):
    @abstractmethod
    def sound(self):
        pass

class Cat(Animal):
    def sound(self):
        print("Meow")

class Dog():
    def sound(self):
        print("Bow")

class Book():
    pass

Animal.register(Dog)

def output(animal):
    print(animal.__class__.__name__, end=": ")
    animal.sound()

if __name__ == "__main__":
    c = Cat()
    output(c)

    d = Dog()
    output(d)

    b = Book()
    output(b)

のような実装をして実行をすると下記のような結果とruntimeエラーを得ることになります。

Cat: Meow
Dog: Bow
AttributeError: 'Book' object has no attribute 'sound'

ダック・タイピングてきにはsoundの無いBookは「鳴かない=動物」ではないためOKです。ですが、できればエラーは検知したいです。
ここは動的型付けなのでしょうがない感は否めないのですが、できれば回避したいので下記のようにします。

案1. try~except

def output(animal):
    print(animal.__class__.__name__, end=": ")
    try:
        animal.sound()
    except AttributeError:
        print('No sound')

tryでexception吐いたらキャッチする方法ですが、実行ベースとなるため実際に例外となってからしかハンドリングできません。あまりオススメはできません。

案2. hasattr

def output(animal):
    if not hasattr(animal, 'sound'):
        return
    print(animal.__class__.__name__, end=": ")
    animal.sound()

Attributeが存在するか否かをチェックします。まぁまぁ妥当です。

案3. isinstance

def output(animal):
    if not isinstance(animal, Animal):
        return
    print(animal.__class__.__name__, end=": ")
    animal.sound()

isinstanceで指定したクラスかどうかを判定します。これなら意図した抽象クラスがハンドリングできます。

mypy

静的型付けチェックを行うmypyというものもありました。静的型付けに慣れている人だと安心できますね。

From Python...

def fib(n):
    a, b = 0, 1
    while a < n:
        yield a
        a, b = b, a+b

...to statically typed Python

def fib(n: int) -> Iterator[int]:
    a, b = 0, 1
    while a < n:
        yield a
        a, b = b, a+b

でも、動的型付け言語はそれはそれで地位を築いているため、いまさら静的にするのも微妙な気はしますね。

おわりに

Pythonでも抽象クラスを実装できるのはよいですがエラーのでるタイミングがなんとも言えない感じです。
抽象化については各言語で仕様が固まりつつも概念が若干違ったりするので面白いです。言語の特性を感じる気がします。