未経験からIT業界に入って2年目、まだまだ知らないことだらけですがなんやかんやで頑張っています。
さて、今回私はこういう勉強をしました。
-
json.load
を使ってjson→pythonへデコードしよう -
dict
を挟まず一発でオブジェクトを作成しよう
順を追って話しましょう。
jsonでこんな設定ファイルを作っていたとします。
{
"class": "Hoge",
"name": "hoge1",
"hugas": [
{
"class": "Huga",
"name": "huga1"
},
{
"class": "Huga",
"name": "huga2"
}
]
}
Hoge
がHuga
を複数持つ設定です。
このファイルに書かれたクラス情報をそのまま、一発でオブジェクトを生成したいです。
1.準備
クラスを実装しておきます。
class Huga:
def __init__(self, name: str):
self.name = name
class Hoge:
def __init__(self, name:str, huga: Huga):
self.name = name
self.hugas = [huga]
可読性のために引数の型を明示しています。
2.試行錯誤
2.1 勉強前だったらこうしてる
一番素直にするなら、json.load
でpythonの辞書形式でデコードしてから、それらの値を逐次的に入れていく方法でしょうか。
import json
############
# 上記のクラス
############
def main():
f = open("path/to/cfg.json", "r")
data = json.load(f)
# data = {'class': 'Hoge', 'name': 'hoge1',
#'hugas': [{'class': 'huga', 'name': 'huga1'},
# {'class': 'huga', 'name': 'huga2'}]}
huga_1 = Huga(name=data['hugas'][0]['name'])
huga_2 = Huga(name=data['hugas'][1]['name'])
print(huga_1, type(huga_1), huga_1.name)
# <__main__.Huga object at 0x7fe7244f7850> <class '__main__.Huga'> huga1
print(huga_2, type(huga_2), huga_2.name)
# <__main__.Huga object at 0x7fe7244f7700> <class '__main__.Huga'> huga2
hoge = Hoge(name=data['name'])
print(type(hoge), hoge.name)
# <class '__main__.Hoge'> hoge1
hoge.add_huga(huga_1)
hoge.add_huga(huga_2)
print(hoge.hugas)
# [<__main__.Huga object at 0x7fe7244f7850>,
# <__main__.Huga object at 0x7fe7244f7700>]
順番としてはこんな感じです。
- Hugaオブジェクトを作成 (huga_1, huga_2)
- Hogeオブジェクトを作成(hoge)
- hogeにhuga_1, huga_2を持たせる
(もっとこう、パパッとできないだろうか...)
2.2 デコード関数を自作する
調べたら、json.load
にobject_hook
という引数があり、ここに"こんな感じでデコードしてほしい"という関数を入れるとその通りにしてくれるらしい。
というわけで早速、作ってみました。
まずは各クラスに自分の型のオブジェクトを返す関数を作成します。
class Huga:
def __init__(self, name: str):
self.name = name
# new
@staticmethod
def hook(obj):
return Huga(name=obj["name"])
class Hoge:
def __init__(self, name: str, huga: Huga):
self.name = name
self.hugas = [huga]
def add_huga(self, huga: Huga):
self.hugas.append(huga)
# new
@staticmethod
def hook(obj):
return Hoge(name=obj["name"], huga=obj["hugas"])
どちらも引数として与えられた辞書をもとに、新しいオブジェクトを返す関数です。
object_hook
に渡す関数はこれらを組み合わせて以下のように作りました。
def custom_decode(obj):
if isinstance(obj, dict):
if obj["class"] == "Hoge":
obj = Hoge.hook(obj)
return obj
elif obj["class"] == "Huga":
obj = Huga.hook(obj)
return obj
else:
return obj
else:
return obj
(設定ファイルにありながら使われてこなかったclass
キー、ここで初登場です)
class
キーの値に対応する関数を呼び出してオブジェクトを生成するか、何もせず返す関数です。
実行してみましょう。
def main():
cfg = "./cfg.json"
f = open(cfg, "r")
ent = json.load(f, object_hook=custom_decode)
print(ent)
# <__main__.Hoge object at 0x7f55bdfc6940>
print(ent.name)
# hoge1
print(ent.hugas)
# [[<__main__.Huga object at 0x7f55bdfeeb50>,
# <__main__.Huga object at 0x7f55bdfc6640>]]
for hu in ent.hugas[0]:
print(hu.name)
# huga1
# huga2
設定ファイルの通りにオブジェクトの作成ができました!
2.3 あれ?
よく見たらent.hugas
がリストの中にリストが入っています。これはして欲しい挙動と少し違います。
デバッグしてみると、以下のような挙動をしてました。
# cfg.json読み込み時
{'class': 'Hoge', 'name': 'hoge', 'hugas': [{'class': 'Huga', 'name': 'huga1'},{'class': 'Huga', 'name': 'huga2'}]}
# custom_decode:1回目
{'class': 'Hoge', 'name': 'hoge', 'hugas': [<__main__.Huga object at 0x7f55bdfeeb50>,{'class': 'Huga', 'name': 'huga2'}]}
# custom_decode:2回目
{'class': 'Hoge', 'name': 'hoge', 'hugas': [<__main__.Huga object at 0x7f55bdfeeb50>,<__main__.Huga object at 0x7f55bdfc6640>]}
# custom_decode:3回目
<__main__.Hoge object at 0x7f55bdfc6940>
分かったことは2つ。
- 呼び出される辞書は末端から。
- リストは保持される。
2つ目に関しては公式のドキュメントに書いてありましたね。厳密には[ ]
はjsonではarrayであり、pythonオブジェクトに変換する際はlistに置き換えられます。
2.4 じゃあどうするか
hook()関数は直しません。__init__()
を直します。
Huga
オブジェクト単体を受け取っていたのを、Huga
オブジェクトが格納されたリストを受け取るように変更しました。
from typing import List
class Hoge:
# def __init__(self, name: str, huga: Huga):
def __init__(self, name: str, hugas: List[Huga]):
self.name = name
# self.hugas = [huga]
self.hugas = hugas
def add_huga(self, huga):
self.hugas.append(huga)
@staticmethod
def hook(obj):
return Hoge(name=obj["name"], huga=obj["hugas"])
もちろんhook()
を直す考え方もあるのでしょうが、よく考えたらHoge
はHuga
のリストを持つことがcfg.json
で決められているのでこのようにしたほうが設定に忠実です。
ちなみにhugas
の型については単にlist
でもいいのですが、個人的にはこちらのほうが可読性が上がるかと思います。
このようにして、コードの全体はこんな形にまとまりました。
import json
from typing import List
class Huga:
def __init__(self, name: str):
self.name = name
@staticmethod
def hook(obj: dict):
return Huga(name=obj["name"])
class Hoge:
def __init__(self, name: str, hugas: List[Huga]):
self.name = name
self.hugas = hugas
def add_huga(self, huga: Huga):
self.hugas.append(huga)
@staticmethod
def hook(obj: dict):
return Hoge(name=obj["name"], hugas=obj["hugas"])
def custom_decode(obj: dict):
if isinstance(obj, dict):
if obj["class"] == "Hoge":
obj = Hoge.hook(obj)
return obj
elif obj["class"] == "Huga":
obj = Huga.hook(obj)
return obj
else:
return obj
else:
return obj
def main():
cfg = "./cfg.json"
f = open(cfg, "r")
ent = json.load(f, object_hook=custom_decode)
print(ent)
# <__main__.Hoge object at 0x7f7913fe9490>
print(ent.name)
# hoge1
print(ent.hugas)
# [<__main__.Huga object at 0x7f7913fe9250>,
# <__main__.Huga object at 0x7f7913fe92b0>]
for hu in ent.hugas:
print(hu.name)
# huga1
# huga2
if __name__ == "__main__":
main()
2.4 結論
今回やりたかったことは「jsonファイルの内容から一発でオブジェクトを生成すること」で、そのためには
- 自作クラスの型のオブジェクトを生成する関数を作り、
- またそれらを使い分ける関数を作り、
-
json.load
のobject_hook
引数に渡す。
ことをすればよい、という話でした。
自分でいちいちオブジェクトを作らなくてよくなりました~
3 感想
やってみたら簡単な話でした。調べてもなかなか出てこなかったのは簡単で発信する程のことでもなかったから...?
でもいいんです、私は確かに学んだのだ。
あと型ヒントは単に想定した型を明示してるだけで、それと異なる型が渡されてもスルーされるっぽいことが分かりました。なんとなくで使ってるとよくないので、ここら辺はまた別でちゃんと調べてみます。