読者です 読者をやめる 読者になる 読者になる

cocuh's note

type(あうとぷっと) -> 駄文

pythonの標準ライブラリabcの紹介

python advent calendar*1の6日目担当の こく(@cocuh)です。

qiita.com

今回は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.SetSized, 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ライフを!

*1:確認しただけで3つもある…全部読んでる方いらっしゃるのかなぁ…?

*2:duck typingがあるので細かく見れば別物ですが、大きく見れば同じようなものを保証しようとするものなのでそう書きました