singledispatchとjson.dumpが相性良いかも?

jsonのencodeエラーについては昔に書いたこの辺を見てもらうとして。

今回の主題は、json.dumpsに渡すdefaultの関数としてfunctools.singledispatchが有用かもという話。

使いかた

singledispatchを利用したモジュールを定義

例えば以下のようなextjsonモジュールを定義してあげる。

extjson.py

import json
from functools import (
    singledispatch,
    partial,
)


@singledispatch
def encode(o):
    raise TypeError("Object of type '%s' is not JSON serializable" % o.__class__.__name__)


register = encode.register

dump = partial(json.dump, indent=2, ensure_ascii=False, sort_keys=True, default=encode)
dumps = partial(json.dumps, indent=2, ensure_ascii=False, sort_keys=True, default=encode)
load = json.load
loads = json.loads

extjsonのdump,dumpsには、defaultに定義したencode()を渡してあげる。このencode()はsingledispatchを利用したもの。

普通に使う

特に何も設定していなくても普通のjsonモジュールと同じように使える。

import extjson

d = {
    "name": "foo",
    "age": 20
}

print(extjson.dumps(d))

# {
#   "age": 20,
#   "name": "foo"
# }

通常ならTypeErrorが出るものに対して使う

通常なら意図しなかった型の値が渡された場合にはTypeErrorになる。このような型に対する対応をsingledispatch.registerで良い感じに使う側でできる。 たとえば、datetimeオブジェクトなどは対応していない型。

import datetime as dt
import extjson


@extjson.register(dt.datetime)
def encode_datetime(o):
    return o.isoformat()


d = {
    "name": "foo",
    "birth": dt.datetime.now(),
}
print(extjson.dumps(d))

# {
#   "birth": "2017-09-24T05:28:16.413651",
#   "name": "foo"
# }

悪くないような気がする。

追記:singledispatchはabcにも対応している

singledispatchはabcにも対応しているので以下の様な形で仮想継承を経由しても使える。

date,datetimeを自分で定義したStringerというクラスに所属させてみた。

import datetime as dt
import extjson
import abc


class Stringer(abc.ABC):
    pass


Stringer.register(dt.datetime)
Stringer.register(dt.date)


@extjson.register(Stringer)
def encode_stringer(o):
    return str(o)


d = {
    "name": "foo",
    "birth": dt.datetime.now(),
    "birthday": dt.date.today(),
}
print(extjson.dumps(d))

# {
#   "birth": "2017-09-24 12:20:27.933120",
#   "birthday": "2017-09-24",
#   "name": "foo"
# }

jqfpyにchunkを追加した。直接batch requestを叩けるように(続く)。

github.com

jqfpyにchunkを追加した。直接batch requestを叩けるように。ただまだ機能は不足しているような気がする。試しにelastic searchのbatch apiを叩いてみようと思ったけれど。まだ足りなそうな感じ(続く)。

欲しくなった理由

例えばテキトウなJSONを出力するコードがあるとする。正確には、データができ次第標準出力にJSONを出力するようなコード。 そのような出力は--slurp付きで呼び出すと、jqfpy上では1つの配列として扱うことができる。

$ seq 1 10
1
2
3
4
5
6
7
8
9
10

$ seq 1 10 | jqfpy --slurp -c
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

これに加えて利用するデータが大きすぎる場合に適宜細かい単位に区切ってあれこれしたい。そのための機能。

h.chunk()

h.chunkは以下の様な感じ。

$ seq 1 10 | jqfpy --slurp -c 'h.chunk(get(), n=3)'
[[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]

3つごとに取り出してみている。(n=3はkeyword only argumentsなのだけれど。制限を緩めたほうが使いやすいかもしれない。h.chunk(L, 3)と書けるように)

1行毎のchunked JSON

ちなみに --squash-c を組み合わせると、1行毎にN個ずつまとめられたJSONが返るようになるので便利。

$ seq 1 10 | jqfpy --slurp -c 'h.chunk(get(), n=3)' --squash
[1, 2, 3]
[4, 5, 6]
[7, 8, 9]
[10]

例えば、xargsと一緒に使ってみる。

$ seq 1 10 | jqfpy --slurp -c 'h.chunk(get(), n=3)' --squash | xargs -I{} echo get data -- {}  --.
get data -- [1, 2, 3] --.
get data -- [4, 5, 6] --.
get data -- [7, 8, 9] --.
get data -- [10] --.

あるいは、bashでループする。

$ seq 1 10 | jqfpy --slurp -c 'h.chunk(get(), n=3)' --squash | (while read LINE; do echo got: $LINE; done)
got: [1, 2, 3]
got: [4, 5, 6]
got: [7, 8, 9]
got: [10]

連番を振りたい場合があるかも?

連番を振りたい場合があるかも?ちょっと複雑な何かを作りたい時に一時的にファイルに書き出したいということはある。

これにはまだちょっと対応できない。空でbashのループを書くのもだるいし。何か良い方法は見つけたい。

一応、暫定的な処置としては以下の様な形でbashの範囲で無理やり書くという方法はある。

$ seq 1 10 | jqfpy --slurp -c 'h.chunk(get(), n=3)' --squash | (i=0; while read LINE; do i=$(($i + 1)); echo "got($i): $LINE"; done)
got(1): [1, 2, 3]
got(2): [4, 5, 6]
got(3): [7, 8, 9]
got(4): [10]

例えば、こういう感じに。

$ seq 1 10 | jqfpy --slurp -c 'h.chunk(get(), n=3)' --squash | (i=0; while read LINE; do i=$(($i + 1)); echo $LINE | jqfpy > data$i.json; done)

$ ls
data1.json  data2.json  data3.json  data4.json

$ cat data1.json
[
  1,
  2,
  3
]

細かい話をすると

細かい話をすると、--slurpと一緒に--unbufferedを使うことには対応していないので、一度すべてのデータを集めてからchunkするという形になっている。なので処理が終わらないとフィルタとしてのjqfpyは実行されないし。省メモリーでもない。