任意クラスをネストした構造への JSON読込みオブジェクト変換は、json.load() の結果のdict:辞書を
丹念にパースすればできますが、膨大なコードを書きたくありません。
json.load() の object_hook でなんとかならないのかと悩みましたが、日付時刻型などは対応できても
任意クラスの自由な構造を許可する構造を汎用的に書くには無理があります。
日付時刻型だけをobject_hook でなんとかするとして、
任意クラスの自由な構造は、やはり JSONDecoder を継承して最後のdict:辞書を解析するしかないようです。
object_hook で、日付時刻型も対処したJSONDecoder継承にすれば、良いようです。
JSONファイル:item.json
{
"name": "Apple",
"price": 1200,
"flag": false,
"mount": [
120,
140
],
"current": 1.02,
"latest": "2020-12-11 09:46:26",
"history": [
{
"version": 1.0,
"updated": "2020-12-01 10:00:00"
},
{
"version": 1.01,
"updated": "2020-12-09 22:30:52"
}
]
}
このJSONを読込んで、構成するクラス Item と ItemHistory
from datetime import datetime
from pytz import timezone
from itemhistory import ItemHistory
class Item():
def __init__(self):
pass
@property
def _name(self):
return self.name
@_name.setter
def _name(self, value):
self.name = value
@property
def _price(self):
return self.price
@_price.setter
def _price(self, value):
self.price = value
@property
def _flag(self):
return self.flag
@_flag.setter
def _flag(self, value):
self.flag = value
@property
def _mount(self):
return self.mount
def addMount(self, value):
self.mount.append(value)
@property
def _current(self):
return self.current
@_current.setter
def _current(self, value):
self.current = value
@property
def _latest(self):
return self.latest
@_latest.setter
def _latest(self, value):
self.latest = value
@property
def _history(self):
return self.history
def addHistory(self):
h = ItemHistory()
h.version = self.current
h.updated = self.latest
self.history.append(h)
self.current = self.current + 0.01
self.latest = datetime.now(tz=timezone('Asia/Tokyo'))
Item の historyが、ItemHistoryのリストになります
class ItemHistory():
def __init__(self):
self.version = None
from datetime import datetime
from pytz import timezone
self.updated = datetime.now(tz=timezone('Asia/Tokyo'))
@property
def _version(self):
return self.version
@_version.setter
def _version(self, value):
self.version = value
@property
def _updated(self):
return self.updated
@_updated.setter
def _updated(self, value):
self.updated = value
customs.py:JSONDecoder を継承したクラス、CustomDecoder を定義します。
import re
from json import JSONDecoder, JSONDecodeError
from datetime import datetime, date
class CustomDecoder(JSONDecoder):
FLAGS = re.VERBOSE | re.MULTILINE | re.DOTALL
WHITESPACE = re.compile(r'[ \t\n\r]*', FLAGS)
def __init__(self, decode_hook = None):
JSONDecoder.__init__(self, object_hook=self.object_hook)
self.decode_hook = decode_hook
self._rt = [
(
re.compile(
'^\d{4}\-(0[1-9]|1[012])\-(0[1-9]|[12][0-9]|3[01]) (0[0-9]|1[0-9]|2[0-3]):(0[0-9]|[0-5][0-9]):(0[0-9]|[0-5][0-9])$'),
'%Y-%m-%d %H:%M:%S'
),
(
re.compile(
'^\d{4}\-(0[1-9]|1[012])\-(0[1-9]|[12][0-9]|3[01])T(0[0-9]|1[0-9]|2[0-3]):(0[0-9]|[0-5][0-9]):(0[0-9]|[0-5][0-9])$'),
'%Y-%m-%dT%H:%M:%S'
),
(
re.compile(
'^\d{4}/(0[1-9]|1[012])/(0[1-9]|[12][0-9]|3[01]) (0[0-9]|1[0-9]|2[0-3]):(0[0-9]|[0-5][0-9]):(0[0-9]|[0-5][0-9])$'),
'%Y/%m/%d %H:%M:%S'
),
(
re.compile('^\d{4}\-(0[1-9]|1[012])\-(0[1-9]|[12][0-9]|3[01])$'),
'%Y-%m-%d'
),
(
re.compile('^\d{4}/(0[1-9]|1[012])/(0[1-9]|[12][0-9]|3[01])$'),
'%Y/%m/%d'
),
]
def object_hook(self, obj):
if isinstance(obj, dict):
for k, v in obj.items():
obj[k] = self.object_hook(v)
elif isinstance(obj, list):
for i, e in enumerate(obj):
obj[i] = self.object_hook(e)
elif isinstance(obj, str):
try:
dlst = [datetime.strptime(obj, rtt[1]) for rtt in self._rt if rtt[0].match(obj)]
if len(dlst) > 0:
obj = dlst[0]
except (ValueError, AttributeError):
pass
return obj
def decode(self, s, _w=WHITESPACE.match):
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
end = _w(s, end).end()
if end != len(s):
raise JSONDecodeError("Extra data", s, end)
return self.decode_hook(obj)
dict:(辞書)から任意のクラスオブジェクトに変換する為のメソッド
=decode_hook に指定するメソッドを定義します。
from item import Item
def item_parser(obj):
item = Item()
for k,v in obj.items():
if k=='history':
if isinstance(obj[k], list):
res = list()
for _o in obj[k]:
h = ItemHistory()
for _k,_v in _o.items():
setattr(h, _k, _v)
res.append(h)
else:
res = None
setattr(item, k, res)
else:
setattr(item, k, v)
return item
'history' キー名で読込んだものをリストとして、ItemHistory を生成するようにしてます。
CustomDecoder をcls= で指定、decode_hook をパラメータ追加で呼出し実行します。
import codecs
import json
from customs import CustomDecoder
with codecs.open('item.json', 'r', 'utf-8') as f:
i = json.load(f, cls=CustomDecoder, decode_hook=item_parser)
結果をtype() で調べると
print(type(i))
print(i.name)
print(i.price)
print(i.current)
print(i.flag)
print(i.mount)
print(i.latest)
print(type(i.latest))
print(i.history)
for h in i.history:
print(type(h))
print(h.version)
print(h.updated)
print(type(h.updated))
decode_hook に指定するメソッドは、Item クラスのメソッドとして
定義しても良いと思います。
その場合呼出しは、Item インスタンスからの呼びだしではなくて
staticメソッドとしての呼び出しです。
i = json.load(f, cls=CustomDecoder, decode_hook=Item.item_parser)