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