Pythonでユーザ定義型をサポートする自作jsonパーサ

Pythonは2.6からjsonシリアライズをする標準モジュールのjsonがある。このモジュールの関数loads/dumpsなどを使うと、手軽にデータをjson文字列にしたり、json文字列をPythonデータに変換できるので、Ajaxアプリケーションのサーバ側プログラムを実装したりするのに使える便利なモジュールだ。
日付型のような、jsonがサポートしないデータ型でも、シリアライズでは関数、デシリアライズではクラスを渡すことでカスタムできる。


今回はPythonjsonのパーサを実装した。
標準モジュールjsonと比べてうれしいところは、

こと。木データなんかでも丸ごと簡単にシリアライズできる。

シリアライズしたいクラスは3つの条件を満たさなければならない。

  • 新スタイルクラス(objectクラスを継承している)
  • __encode_json__を定義している
  • __decode_json__(クラスメソッド)を定義している

下のコードで示すと、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()