json-pyのラッパを書いてみた
json-py が意外に使いにくかったので, Pythonの普通のオブジェクトと, JSON文字列の間で直接変換できるように, ちょっとラッパを書いてみた. なんか, いかにも誰かが既に書いていそうなものだけど, まあ, 勉強ということで. json2と命名. 安易だ.
ラッパの設計上問題になったのは, JSON文字列からオブジェクトを構成する際に, どうやってオブジェクトの方情報を渡すか. 先日のエントリでは, テンプレートになるオブジェクトを渡してフィルしてもらう方針で疑似コードを書いてみたのだが, これだとネストしたオブジェクトの取り扱いが難しそうなので断念. クラス変数で型宣言する方法にしてみた.
構造
- 変換対象のPythonオブジェクトはクラス変数で, 各インスタンス変数の型を宣言する. これはDjangoとか, Google App Engine が Modelの各フィールドの型宣言に使っているのと同じ手法.
- 型の宣言には, str, int, float, long, および 他のクラス名, これらを含んだリスト, ディクショナリが使用できる.
- JSON文字列から変換する場合には, 第二引数で型を指定する.
使い方
宣言付きのオブジェクトはこんな感じ. このクラスは, ホスト名とポートからなる host というクラスを定義している. __repr__は, もちろん, なくてもいい.
class host(object): hostname = str port = int def __init__(self, _hostname = None, _port = None): self.hostname = _hostname self.port = _port def __repr__(self): return "host( hostname = %s, port = %d)" % (self.hostname, self.port)
これをJSONに変換するとこんな感じ.
>>> import json2 >>> json2.write(host('localhost', 8080)) '{"hostname":"localhost","port":8080}'JSON文字列を読む時には型を指定する>>> json2.read('{"hostname":"localhost","port":8080}', host) host( hostname = localhost, port = 8080)ネストしたオブジェクトも処理できる.class hostGroup(object): name = str hosts = [host] def __init__(self, _name = None, _hosts = []): self.name = _name self.hosts = _hosts def __repr__(self): return "%s: [%s]" % (self.name, ",".join(str(host) for host in self.hosts))このクラスは hostのリストをhostsフィールドに持つ. このクラスをJSON文字列に変換.>>> json2.write(hostGroup("groupA", [host("localhost", 8000), host("example.com", 80)])) '{"hosts":[{"hostname":"localhost","port":8000},{"hostname":"example.com","port":80}],"name":"groupA"}' >>>戻すときには型名を指定.>>> json2.read("""{"hosts":[ ... {"hostname":"localhost","port":8000}, ... {"hostname":"example.com","port":80}], ... "name":"groupA"}""" ... , hostGroup) groupA: [host( hostname = localhost, port = 8000),host( hostname = example.com, port = 80)] >>>ソース
自分が使う範囲でしか動作確認していません. エラーハンドルも適当. json2.py というファイル名でセーブすれば, 上のサンプルは動くはず.import json import logging """ This module helps to transform python object to/from json string. It assumes that python objects declares its fields type as class fields, like django models. It also assumes that the instance can be created without arguments; i.e. __init__ allows no argument instance creation. """ def write(o): """ to json string """ return json.write(_toPlain(o)) def read(s, t): return _fromPlain(json.read(s), t) def _toPlain(o): if isinstance(o, str) or isinstance(o, int) \ or isinstance(o, long) or isinstance(o, float) \ or isinstance(o, bool) or o == None: return o if isinstance(o, list): res = for item in o: res.append(_toPlain(item)) return res if isinstance(o, dict): res = {} for key in o.keys(): res[key] = _toPlain(o[key]) return res # looks like object res = {} for key in o.__class__.__dict__: if not key.startswith("__") and o.__dict__.has_key(key): res[key] = _toPlain(o.__dict__[key]) return res def _fromPlain(p, t): if p == None: return p if t == str or t == int or t == long \ or t == float or t == bool: if isinstance(p, t): return p logging.error("type mismatch: %s is '%s', while type specified was %s", (str(p), str(type(p)), str(t))) return p if isinstance(p, list) and isinstance(t, list): res = for i in range(len(p)): item = p[i] res.append(_fromPlain(item, t[i % len(t)])) return res if isinstance(p, dict): if isinstance(t, dict): res = {} for key in p.keys: res[key] = _fromPlain(p[key], t.values[0]) return res else: # to object res = t() for key in t.__dict__: if not key.startswith("__"): res.__dict__[key] = _fromPlain(p[key], t.__dict__[key]) return res else: logging.error("cannot handle")
- 7月1日修正. Noneとboolに対応
- 7月2日修正. ディクショナリ関連のバグ修正.