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

この記事は[Python Advent Calendar 2015](http://qiita.com/advent-calendar/2015/python)の8日目の記事です。


-----

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

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

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


## ABC - Abstract Base Classes モジュール

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

```python
#!/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エラーとなります。（※インスタンス生成時）

```python
class Cat(Animal):
    pass

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


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

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

```python
#!/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エラーとなります。（※メソッド呼び出し時）

```python
class Dog():
    pass

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

### 抽象メソッドデコレータ

#### `@abstractmethod`

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

```python
#!/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`で記述する。

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


#### `@abstractstaticmethod` (version 3.2)

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

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


## ダック・タイピング

抽象クラスについては上記で十分ですが、ここまでやったら[ダック・タイピング](https://ja.wikipedia.org/wiki/ダック・タイピング)も調べておきたいと思ったので実装してみます。

### ダック・タイピングとは？

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

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

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


### 実装

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

```python
#!/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`

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

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


#### 案2. `hasattr`

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

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


#### 案3. `isinstance`

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

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


## [mypy](https://github.com/JukkaL/mypy)

静的型付けチェックを行う[mypy](https://github.com/JukkaL/mypy)というものもありました。静的型付けに慣れている人だと安心できますね。

_From Python..._

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

_...to statically typed Python_

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

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

## おわりに

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