Pythonの標準ライブラリ、jsonモジュールの基本操作をまとめます。
環境
Python 3.8.10
Ubuntu 20.04.3
基本操作
json.load
対象ファイルからjsonオブジェクトを受け取る。返り値は辞書型
with open('jsonファイルのパス', 'r') as f:
map_obj = json.load(f)
print(type(map_obj) # <class 'dict'>
json.loads
文字列からjsonオブジェクトを受け取る。返り値は辞書型
str = '{"a": 1, "b": 2, "c": 3}'
map_obj = json.loads(str)
print(type(map_obj)
# >> <class 'dict'>
json.dump
辞書型の値をjsonに変換してファイル出力する。第一引数には変換するデータ、第2引数には、出力先のファイルを指定する。
str = '{"a": 1, "b": 2, "c": 3}'
with open('jsonファイルのパス', 'w') as f:
json.dump(str, f)
整形したい場合は、第三引数に指定する
str = '{"a": 1, "b": 2, "c": 3}'
with open('jsonファイルのパス', 'w') as f:
json.dump(str, f, indent=2)
json.dumps
辞書型の値をjsonに変換しm、文字列として出力する。
map = {a: 1, b: 2, c: 3}
print(json.dump(str)) # '{"a": 1, "b": 2, "c": 3}'
エンコード、デコードのカスタマイズ
Pythonのjsonモジュールでエンコード・デコードできるデータ型は以下に限られている。
JSON ⇔ Python
- object ⇔dict
- array ⇔ list
- string ⇔ str
- number(int) ⇔ int
- number(real) ⇔ float
- true ⇔ True
- false ⇔ False
- nukk ⇔ None
そのため、他のデータ型を変換したい場合は、独自の関数を作成し、エンコーダーとデコーダーに渡してあげる必要がある。
エンコードのカスタマイズ(datetime型)
import json
from datetime import datetime
profiles = [
{'Name': 'Tarou', 'Age': 15, 'Gender': 'male', 'birth_day': datetime(2000, 9, 11)},
{'Name': 'Sayaka', 'Age': 20, 'Gender': 'female', 'birth_day': datetime(2000, 3, 11)},
]
def main():
# dampsのclsにDatetimeJSONEncoderを渡す。
# こうすることでデフォルトの方式ではなく
# DatetimeJSONEncoderに定義したdefaultにしたがってエンコードされる。
json_data = json.dumps(profiles, indent=4, cls=DatetimeJSONEncoder)
print(json_data)
class DatetimeJSONEncoder(json.JSONEncoder):
"""JSONEncoderのdefaultを上書きする"""
def default(self, o):
if isinstance(o, datetime):
# datetime型の時は、isoformatで文字列を変える。toordinalとかでも良い。
return {'_type': 'datetime', 'value': o.isoformat()}
else:
# datetime型ではないときは、通常の方法の、defaultでエンコードする。
# super()はjson.JSONEncoderと書いても良い。
return super().default(o)
if __name__ == '__main__':
main()
出力結果
[
{
"Name": "Tarou",
"Age": 15,
"Gender": "male",
"birth_day": {
"_type": "datetime",
"value": "2000-09-11T00:00:00"
}
},
{
"Name": "Sayaka",
"Age": 20,
"Gender": "female",
"birth_day": {
"_type": "datetime",
"value": "2000-03-11T00:00:00"
}
}
]
受け取ったdatetime型を返す時に、'_type'プロパティを与えているのは、複数のデータ型に対応するため、およびデコードするための前準備です。
return {'_type': 'datetime', 'value': o.isoformat()}
というのも、、、エンコードするときは、データ型が分かります。
しかし、jsonからデコードするとき、jsonはただの文字列なので、データ型が判断できません。そのため、デコードするときに備えて、データの型を保存しておく必要があります。
エンコードのカスタマイズ(クラス)
import json
class Person():
def __init__(self, name, age=0):
self.name = name
self.age = age
def main():
bob = Person("Bob", 23)
bob_json = json.dumps(bob, indent=4, cls=PersonEncoder)
print(bob_json)
class PersonEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, Person):
# classの情報を辞書型で取得するために、__dict__を仕様
# あまり使わない方がよい、という記事もあり、ベストな書き方かは不明
return {'_type': 'Person', 'value': o.__dict__}
else:
return json.JSONEncoder.default(o)
if __name__ == '__main__':
main()
出力結果
{
"_type": "Person",
"value": {
"name": "Bob",
"age": 23
}
}
datetime型の時とほとんど同じなので、特別な解説は不要かと思います。
デコードのカスタマイズ(datetime型)
import json
from datetime import datetime
profiles_json = '''[
{
"Name": "Tarou",
"Age": 15,
"Gender": "male",
"birth_day": {
"_type": "datetime",
"value": "2000-09-11T00:00:00"
}
},
{
"Name": "Sayaka",
"Age": 20,
"Gender": "female",
"birth_day": {
"_type": "datetime",
"value": "2000-03-11T00:00:00"
}
}
]'''
def main():
profiles = json.loads(profiles_json, cls=DatetimeDecoder)
print(profiles[0])
print(f'birth_day: {profiles[0]["birth_day"]}')
print(f'type of birth_day: {type(profiles[0]["birth_day"])}')
class DatetimeDecoder(json.JSONDecoder):
"""json.JSONDecorderのobject_hookを上書きする"""
# __init__で何をやっているかはいまいち分かっていない。
def __init__(self, *args, **kwargs):
json.JSONDecoder.__init__(self, object_hook=self.object_hook, *args, **kwargs)
def object_hook(self, o):
if '_type' not in o:
# _typeが定義されていない。つまり、jsonモジュールで変換がサポートされている型はそのまま返す。
return o
type = o['_type']
if type == 'datetime':
# _typeがdatetimeの場合、datetimeに変換して返す。。
return datetime.fromisoformat(o['value'])
if __name__ == '__main__':
main()
出力結果
{'Name': 'Tarou', 'Age': 15, 'Gender': 'male', 'birth_day': datetime.datetime(2000, 9, 11, 0, 0)}
birth_day: 2000-09-11 00:00:00
type of birth_day: <class 'datetime.datetime'>
datetime型でしっかり取得できているのが分かります。先ほども述べましたが、jsonは単なる文字列ではあるので、_typeのような形で型情報がなければ、このように正しいデータ型を取り出すのは難しいです。
デコードのカスタマイズ(クラス)
import json
def main():
bob_json = '{"_type": "Person","value": {"name": "Bob","age": 23}}'
person = json.loads(bob_json, cls=PersonDecoder)
print(f'type: {type(person)}')
print(f'name: {person.name}')
print(f'age: {person.age}')
class Person():
def __init__(self, name, age=0):
self.name = name
self.age = age
class PersonDecoder(json.JSONDecoder):
"""json.JSONDecorderのobject_hookを上書きする"""
# __init__で何をやっているかはいまいち分かっていない。
def __init__(self, *args, **kwargs):
json.JSONDecoder.__init__(self, object_hook=self.object_hook, *args, **kwargs)
def object_hook(self, o):
if '_type' not in o:
# _typeが定義されていない。つまり、jsonモジュールで変換がサポートされている型はそのまま返す。
return o
type = o['_type']
if type == 'Person':
# _typeがPersonクラスのものは、クラスのインスタンスとして返す。
return Person(**o['value'])
if __name__ == '__main__':
main()
出力結果
type: <class '__main__.Person'>
name: Bob
age: 23
datetime型の変換とほぼ一緒です。
エンコードとデコート、datetime型とクラスすべて合わせる
分かりやすくするためにdatetimeとクラスを分けて書いていたのですが、一緒に書けます。
class MyEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, date):
return {'_type': 'date', 'value': o.isoformat()}
elif isinstance(o, Person):
return {'_type': 'Person', 'value': o.__dict__}
else:
return json.JSONEncoder.default(self, o)
class MyDecoder(json.JSONDecoder):
def __init__(self, *args, **kwargs):
json.JSONDecoder.__init__(self, object_hook=self.object_hook, *args, **kwargs)
def object_hook(self, o):
if '_type' not in o:
return o
type = o['_type']
if type == 'date':
return date.fromisoformat(o['value'])
elif type == 'Person':
return Person(**o['value'])
if文にほかの条件も書いてあげれば、jsonがデフォルトではサポートしていないデータ型も、柔軟に変換できるようになります。試してみてください。
他にももっといい書き方があると思いますので、分かる方がいたら教えてください!