仮想継承とsingledispatch
以前、emitjson というパッケージを公開した。使い方は簡単に こちらに 書いたが、要はfunctools.singledispatch() をちょっと使いやすくしたものにすぎない。
singledispatch()
は、Python3の抽象基底クラスと組み合わせると、非常に面白い使い方ができる。emitjson
を例にして、使い方を紹介してみたい。
emitjson
簡単に emitjson
の使い方を說明しておこう
emitjson
は、emitjson
という名前だが、JSON
を出力するためのモジュールではなく、いろいろなオブジェクトを json
として出力可能なオブジェクトに変換するためのリポジトリを作成するモジュールだ。特にJSON専用ということもなく、たとえばCSVファイルを作成する場合などでも利用できる。
例として、datatime.date
型の日付と、PIL.Image.Image
型の画像データを変換するリポジトリを作成してみる。
import emitjson import datetime import base64 import PIL.Image myrepo = emitjson.repository() # emitjson リポジトリを作成 # date型をISO-8601形式の文字列に変換 @myrepo.register(datetime.date) def conv_date(obj): return obj.isoformat() # Image型をBASE64形式の文字列に変換 @myrepo.register(PIL.Image.Image) def conv_date(obj): return base64.b64encode(obj.tobytes()).decode('ascii') # 日付と画像の辞書を作成 src = { 'date':datetime.date.today(), 'img': PIL.Image.open('img.png') } # 辞書に格納した、dateオブジェクトとImageオブジェクトを変換する converted = myrepo(src)
仮想継承
上記の例でわかるように、emitjson
は、オブジェクトの型を指定して、その変換用関数を登録する singledispatch
関数だ。変換関数は、指定した型そのものだけではなく、その派生型のオブジェクトにも適用される。
ここで、「派生型」とは、isinstance()
や issubsclass()
で派生型と判定されるオブジェクトである、ということだ。singledispatch
関数は、型から対象の関数を取得するとき、単純に型から関数を取得するのではなく、オブジェクトの継承関係を調べ、もっとも「近い」型の関数を適用するようになっている。
ところで、 Pythonで isinstance()
や issubsclass()
が派生クラスと判定するのは、通常の継承関係の他に「仮想継承」がある。仮想継承は、直接の継承関係のない型同士であっても、派生クラスと判定されるように設定する機能だ。これを利用すれば、いろいろと面白い singledispatch
の利用方法が考えられる。ちなみに、Pythonの仮想継承はC++言語の同じ用語の機能とはまったく無関係なので注意。
ABCMeta.register()
abcモジュール の ABCMeta
メタクラスを利用すると、手軽にいろいろな型のグループを定義できる。例えば、次のような、2つのクラスがあった時、
class Spam: def __init__(self, n): self.spams = 'spam'*n def __str__(self): return self.spans class Ham: def __init__(self, n): self.hams = 'ham'*n def __str__(self): return self.hams
どちらのクラスのインスタンスもJSONに文字列として出力する関数を、次のように定義できる。
import abc class ToStringTypes(metaclass=abc.ABCMeta): pass # SpamとHamをToStringTypesの仮想サブクラスとして登録する ToStringTypes.register(Spam) ToStringTypes.register(Ham) # ToStringTypesの変換関数 @myrepo.register(ToStringTypes) def toStringConv(obj): return str(obj) # objを文字列化 myrepo(Spam()) # toStringConv(Spam()) が呼び出される
ToStringTypes
は abc.ABCMeta
をメタクラスとするクラスで、抽象基底クラスと呼ばれる。この「抽象基底クラス」も、C++言語の用語とは切り離して考えていただきたい。
ここでは、抽象基底クラスで利用可能な register()
メソッドを使って、Spam
クラスと Ham
クラスを、ToStringTypes
クラスの派生クラスとして登録する。
どちらのクラスも、親クラスとして ToStringTypes
クラスを指定していないが、
isinstance(Spam(), ToStringTypes)`
や、
issubsclass(Ham, ToStringTypes)
は、どちらもTrue
を返すようになる。
したがって、ToStringTypes
オブジェクトの変換関数として toStringConv()
を登録すると、Spam
クラスと Ham
クラスのインスタンスは、toStringConv()
で文字列に変換されるようになる。
この方式では、Spam
クラスと Ham
クラスに一切手を入れずに、変換関数だけを定義できる。Pythonの標準オブジェクトや、サードパーティ製ライブラリのオブジェクトなどでも、ソースを修正せずに柔軟な変換関数を定義できるというのがポイントだ。
subclasshook()
ABCMeta.register()
を使って明示的に仮想サブクラスを登録するのではなく、特定の条件のクラスをサブクラスとして扱うようにもできる。
class HasToDict(metaclass=abc.ABCMeta): @classmethod def __subclasshook__(cls, subclass): return hasattr(subclass, 'toDict')
この抽象基底クラス HasToDict
は、__subclasshook__()
メソッドを定義し、toDict
という名前の属性をもつ型ならなんでも派生クラスとなるようにしている。HasToDict
クラスを利用して、
class Spam: def toDict(self): return {'spam':100} @myrepo.register(HasToDict) def conv_to_dict(obj): return obj.toDict()
のように、toDict()
メソッドをもつオブジェクトはすべて辞書に変換する、という変換関数を定義できる。