このBlogは移転しました。今後は aish.dev を御覧ください。

emitjson

emitjson というPythonモジュールを公開した。

pypi.python.org

emitjson という名前にしたが、じつはJSONは出力しない。 任意のオブジェクトを、json モジュールがサポートしているオブジェクトに変換するときに使うユーティリティだ。

例えば、日付とバイナリデータが入った辞書のリストからJSONを作成しようと思うと、だいたいこんな感じになる。

values = []
for d in src_vlaues:
   date = d['date'].isoformat()
   bin = base64.b64encode(d['bin'])
   values.append({'date':date, 'bin':bin})

json.dumps(values)

意外とめんどくさい。このぐらいなら「めんどくさい」で済むが、要素の数が多いとめんどくささが半端なくなってくる。 json.JSONEncoder をカスタマイズする手はあるが、それもなかなか面倒だ。

そこで、自前でデータを変換する仕組みを考えてみた。collections.abc と、Python3.4で導入された、functools.singledispatch を使えば、簡単に拡張性の高い変換ユーティリティを作れそうだ。

作ってみると、あっさりといい感じに仕上がった。上の処理は次のように書ける。

import emitjson

myrepo = emitjson.repository()  # emitjson リポジトリを作成

# リポジトリに bytes型の変換関数を登録
@myrepo.register(bytes)
def conv_bytes(obj):
    # bytes型をBase64文字列に変換
    return base64.b64encode(obj).decode('ascii')

json.dumps(myrepo(src_vlaues))

emitjson では、リストの要素や辞書のキー・値を走査し、登録した種類のオブジェクトがあれば、自動的に変換を行う。デフォルトでは シーケンス型・マッピング型の操作をサポートしており、datetime.date, datetime.datetime の要素があれば ISO 8601形式の文字列に変換する。

emitjson.repository() をデコレータとして、独自の変換関数を登録できる。上の例では、conv_bytes() 関数は、bytes型のオブジェクトを文字列に変換する。

また、DjangoやSQLAlchemyのモデルなどを辞書オブジェクトに変換する処理は、次のように定義できる。

class Test:
    def __init__(self):
        self.prop1 = 'spam'
        self.prop2 = 'ham'

# Test1クラスを変換する処理の定義
@myrepo.register(Test1)
class Test1Converter(emitjson.ObjConverter):
    prop1 = attr    # obj.prop1 を取得
    prop_a = attr('prop2')    # obj.prop2 を取得

json.dumps(myrepo([Test()]))

同じように、オブジェクトをJSON化可能な形式に変換するモジュールに、bpmappers がある。

bpmappers では主にクラスインスタンスを辞書に変換する機能が主となっているが、emitjson では簡単に任意のオブジェクトを変換できるようになっている。また、コンテナオブジェクトを変換するとその内容を自動的にスキャンし、再帰的に変換を行う点も大きな違いだ。ただし、 emitjson は Python3.4 以降でなければ動かない。Python3.4より前、とくにPython2.xで利用する場合は bpmappers が必須となる。測定はしていないが、おそらくパフォーマンスもbpmappers の方が優れていると思う。