Pythonでユーザ定義型をサポートする自作jsonパーサ
Pythonは2.6からjsonのシリアライズをする標準モジュールのjsonがある。このモジュールの関数loads/dumpsなどを使うと、手軽にデータをjson文字列にしたり、json文字列をPythonデータに変換できるので、Ajaxアプリケーションのサーバ側プログラムを実装したりするのに使える便利なモジュールだ。
日付型のような、jsonがサポートしないデータ型でも、シリアライズでは関数、デシリアライズではクラスを渡すことでカスタムできる。
今回はPythonでjsonのパーサを実装した。
標準モジュールjsonと比べてうれしいところは、
こと。木データなんかでも丸ごと簡単にシリアライズできる。
シリアライズしたいクラスは3つの条件を満たさなければならない。
下のコードで示すと、Rectangleは内部に2つのPointオブジェクトを持つ。Rectangle.__encode_json__ではそれらをリストにして返す。シリアライザは再帰的にシリアライズするので、2つのPointオブジェクトがさらにシリアライズされ最終的にjson文字列が作られる。
class Point(object): def __init__(self, x, y): self.x = x self.y = y def __encode_json__(self): return [self.x, self.y] @classmethod def __decode_json__(cls, val): return cls(val[0], val[1]) def __eq__(p, q): return p.x == q.x and p.y == q.y class Rectangle(object): def __init__(self, p, q): self.p = p self.q = q def __encode_json__(self): return [self.p, self.q] @classmethod def __decode_json__(cls, val): return cls(val[0], val[1]) def __eq__(r, s): return r.p == s.p and r.q == s.q p = Point(0, 0) q = Point(5, 10) r = Rectangle(p, q) d = dumps(r) d # => '{"__class__":"Rectangle","__value__":[{"__class__":"Point", "__value__,0]},{"__class__":"Point", "__value__":[5,10]}]}' r == loads(d, [Rectangle, Point]) # => True
ユーザ定義型をシリアライズするとき、データは内部で下のようなJavaScriptのObject形式に変換される。
{ "__class__": "(クラス名)", "__value__": (データ) }
たとえば、上のPoint(0,0)は
{ "__class__": "Point", "__value__": [0, 0] }
と変換される。Rectangle(Point(0, 0), Point(5, 10))は
{ "__class__": "Rectangle", "__value__": [ {"__class__": "Point", "__value__": [0, 0] }, {"__class__": "Point", "__value__": [5, 10] } ] }
に変換される。
JavaScriptで同じようなモジュールを作れば、サーバとクライアントで透過的なデータのやり取りができるようになるかもしれない。
モジュールはこちら
json_yetanother.py 214行.
こっちはユニットテストスクリプト。
unittest_json_yetanother.py 174行.
このモジュールを使って、文字列をデシリアライズしてTkinterで画面に描画をするプログラム作ってみた。
# coding: utf-8 import Tkinter as tk from json_yetanother import loads,dumps # 自作モジュール class Canvas(object): # canvas def __init__(self, width, height): self.shapes = [] self.width = width self.height = height def draw(self, realcanvas): realcanvas.delete(tk.ALL) for shape in self.shapes: shape.draw(realcanvas) def __encode_json__(self): return {"shapes":self.shapes, "width":self.width, "height":self.height} @classmethod def __decode_json__(cls, dic): canvas = cls(dic["width"], dic["height"]) canvas.shapes = dic["shapes"] return canvas def add(self, shape): self.shapes.append(shape) class Point(object): def __init__(self, x, y): self.x = x self.y = y def __encode_json__(self): return [self.x, self.y] @classmethod def __decode_json__(cls, lst): return cls(*lst) class Line(object): def __init__(self, start, end): self.s = start self.e = end def draw(self, canvas): canvas.create_line(self.s.x, self.s.y, self.e.x, self.e.y) def __encode_json__(self): return [self.s, self.e] @classmethod def __decode_json__(cls, lst): return cls(*lst) class Circle(object): def __init__(self, radius, center): self.r = radius self.c = center def draw(self, canvas): canvas.create_oval( self.c.x - self.r, self.c.y - self.r, self.c.x + self.r, self.c.y + self.r ) def __encode_json__(self): return [self.r, self.c] @classmethod def __decode_json__(cls, lst): return cls(*lst) lastEditContents = """ {"__class__":"Canvas", "__value__": { "width":200, "height":200, "shapes": [ {"__class__":"Line", "__value__":[ {"__class__":"Point", "__value__":[0, 0]}, {"__class__":"Point", "__value__":[200, 200]} ]}, {"__class__":"Circle", "__value__":[ 98, {"__class__":"Point", "__value__":[100, 100] } ]} ] } } """ def main(): canvas = loads(lastEditContents, [Canvas,Point,Line,Circle]) root = tk.Tk() tkcanvas = tk.Canvas(root, width=canvas.width, height=canvas.height) tkcanvas.pack() canvas.draw(tkcanvas) tk.mainloop() if __name__ == '__main__': main()