pythonの標準ライブラリabcの紹介
python advent calendar*1の6日目担当の こく(@cocuh)です。
今回はpythonの 標準ライブラリのabc
と abcを用いた duck typing
を記述する話について話そうかとおもいます。
もし間違ったこと書いていたらコメントにてぜひ教えてください。
対象読者
- オブジェクト指向におけるクラスという概念がなんとなくわかる
- abstract classがわかると特に
- 大規模開発・歴史古いプロジェクト・ドキュメントない事案に遭遇したことがある
- と最後まで楽しめるかもしれないです
abcモジュールとは?
pythonの標準ライブラリにはいっている abc
モジュールで、abcは Abstract Base Class
の略で 抽象基底クラス
のことです。c++やjavaなどいうabstract classをpythonでサポートします*2。
abstract base classとは?
簡単に言えばインスタンスの生成できないクラスのことで、abcをインターフェイス定義やデフォルト実装として継承して使います。
継承しても、インターフェイスで定義されたメソッドが実装されていない場合はエラーを返します。
# -*- coding: utf-8 -*- import abc class AbstractNinja(metaclass=abc.ABCMeta): #python3 # AbstractNinjaはrun methodを持っているという定義 @abc.abstractmethod def run(self): raise NotImplemented() def jump(self): print('ぴょんぴょん') class FutonNinja(AbstractNinja):# AbstractNinjaを継承 # AbstractNinjaで定義してあるインターフェイス、 # runメソッドの処理をoverrideして実装している def run(self): print('(:3っ)っ 三=ー [※※]') def main(): #abstract_ninja = AbstractNinja() # AbstractNinjaにはrunの実装がないため、 # インスタンス生成しようとするTypeErrorが起こる # TypeError: Can't instantiate abstract class AbstractNinja with abstract methods run futon_ninja = FutonNinja() # 動く futon_ninja.run() # overrideしたものが叩ける futon_ninja.jump() # 継承なのでabc側のメソッドが叩ける if __name__ == '__main__': main()
python2,3 compatibleな書き方
# -*- coding: utf-8 -*- import abc class AbstractNinja(): __metaclass__ = abc.ABCMeta @abc.abstractmethod def run(self): raise NotImplemented()
これで何が嬉しいの?
pythonで インターフェイスの定義ができる ということです。
これは、pythonにはjavaでいうinterfaceという言語機能がないためことが大きく起因しています。
蛇足ですが、実はpythonにもinterfaceを入れようという動きが昔(python2.2時代)にあったようです。
PEP 245 -- Python Interface Syntax | Python.org
具体的に嬉しい時を教えて
たとえば、長いことコードを書いている方は、
- 「あれ、このクラスなんのメソッド実装すれば動くんだ…?」とか
- 「このクラス群はどのメソッドと属性を持っているんだ…?」とか
- 「クラスにメソッドが存在するか確認するためにgetattr叩くのやだ…」とか という経験があるとおもいます。その時にabcはそこそこ有用に使えます。
たとえば、pythonの特殊メソッドを用いて 「順序を保持するset型: ListBasedSet」 を実装したいときです。
(特殊メソッドは __init__
とか__iter__
とか__len__
のことです)
簡単に考えればlistとsetを継承してoverrideすればいいのですが、listとsetにあるメソッドを必要に応じてoverrideする必要がありそうです。
このとき、interface定義してあるとsetとして扱うときに必要なメソッドの定義がわかります。
pythonではcollections.abc
にインターフェイスの定義があり、collections.abc.Set
を継承すればよいので。。。
これは標準ライブラリなのでドキュメントがありますが、ドキュメントがなかったりしてソースコードを読むことになった場合は。。。
collections.abc.Set
はSized, Iterable, Container
を継承しており、それぞれのclassは。
class Set(Sized, Iterable, Container): (略) class Sized(metaclass=ABCMeta): __slots__ = () @abstractmethod def __len__(self): return 0 (略) class Iterable(metaclass=ABCMeta): __slots__ = () @abstractmethod def __iter__(self): while False: yield None (略) class Container(metaclass=ABCMeta): __slots__ = () @abstractmethod def __contains__(self, x): return False
https://github.com/python/cpython/blob/master/Lib/_collections_abc.py#L316-L322
「とりあえずこの3つのmethodがあればなんとかなりそう」と検討付ができます。
実装するとこんな感じ
class ListBasedSet(collections.abc.Set): def __init__(self, iterable): self.elements = [] for value in iterable: if value not in self.elements: self.elements.append(value) def __iter__(self): return iter(self.elements) def __contains__(self, value): return value in self.elements def __len__(self): return len(self.elements) s1 = ListBasedSet('abcdef') s2 = ListBasedSet('defghi') overlap = s1 & s2 # __and__()メソッドはに実装があるため実装されなくとも使える
(上記コードはpythonのcollections.abc referenceを少し改変したもの)
今回は継承先がsetだったのでよかったですが、HTTPEndpoint
だったり RoutePolicy
みたいな感じのクラスを拡張しようとすると、どのメソッドを書けばいいのかを知るためドキュメントがあればよいですが、ヘタするとソースコードを解読する必要がでてきてしまいます。
逆に使う側になった時もどのメソッドや属性が利用可能かを知るため、なんのインターフェイスを保証しているか知る必要があります。これらをコード上で保証するのがabcです。
(よさが伝わったらいいな)
duck typingとの併用
動的型付けでduck typingを使っているとあるインターフェイスが定義しているか確認しないといけません。
そのためにgetattr
を叩くのはつらいので、isinstanceを使いたいとします。が、これには継承関係が必要のように思うかもしれません。
import abc class OldNinja:#何も継承していない def hashas(self): print('hashas!!!><') class INinja(metaclass=abc.ABCMeta): #あとから定義されたinterface @abc.abstractmethod def hashas(self): pass INinja.register(OldNinja) ninja = OldNinja() assert isinstance(ninja, INinja)
ということも可能です。
個人的にduck typingにおいても叩くことの可能なmethod・書くべきmethodを(日本語でも何でもいいので)何かしらの形で記述すべきで、
それならばinterfaceを定義しimplementsするPRしてもよいのではと考える人間なので、その面倒を嫌うduck typingは動的型付け言語においては嫌いです。
MRO
pythonが多重継承が可能な関係で、メソッドの名前解決の順番が問題になります。詳しいことは書きませんが、abcのクラスは継承順で後ろの方(右側)にかくとだいたい大丈夫です。
おわりに
使えると思ったら使うといいかも...?
別に使わなくてもよいと思ったら使わなくてよいと想います。使うとjavaっぽいpythonになってしまうので…
業務で書いてたまぁ大きめのpythonのプロジェクトはabc使っていたりします。
本当はmixinまわりの話とmypy絡めた話も書きたかったのですが思った以上に分量が多くなったのでこのあたりでおば。
よいpythonライフを!