はじめに
画像認識・機械学習屋といえども、json形式は避けて通れない。
理由:MS COCOなどのアノテーションはjson形式である。
{"file_name": "0001.png", "objects": {"bbox": [[302.0, 109.0, 73.0, 52.0]], "categories": [0]}}
{"file_name": "0002.png", "objects": {"bbox": [[810.0, 100.0, 57.0, 28.0]], "categories": [1]}}
{"file_name": "0003.png", "objects": {"bbox": [[160.0, 31.0, 248.0, 616.0], [741.0, 68.0, 202.0, 401.0]], "categories": [2, 2]}}
理由:画像認識などのwebサービスの結果のAPIは、json形式である場合が多い。
プログラムの中では、データを保存したり、データを読み出すことが多い。
そのときに、データの形式が異なるたびに、データ保存用関数・データ読み込み用の関数を書くことになる。
class Something:
foo: str
bar: float
みたいなデータ構造があったときに、そのたび、そのデータ構造用の保存関数・読み込み関数を書く時代が長く続いていた。
でも、いまはpydantic を使えば、データ構造用の保存関数・読み込み関数が大幅に簡略化できることを知った。
Json以前
データの書き込み・読み込みは、値の順序や値の大きさが明確にハードコーディングが必要な状況だった。
そのため、データの記述順序が異なるだけで、読み込み不能になるは安定なものだった。
ある項目をデータ構造に追加した時点で、データ構造の後方互換性を破壊してしまっていた。
Json以降
Json以降、データの保存、読み込みが飛躍的に簡単になった。
- キーと値の組合せで書かれているので、数値・文字列の羅列が何を意味しているのかが分からないという状況を脱した。
- キーと値の組合せで書かれているので、記述の順序の影響を受けることが減った。
- jsonファイルという標準の範囲の中での書式のゆらぎの影響を受けなくなった。(空白の有無、改行の有無などの影響を受けない。)
- 言語を超えて利用できる。
### この時点での状況
to_json()メソッド、from_json()メソッドをそれぞれのクラスに対して自分でコーディングする必要がある。
元のclassのデータ構造が変われば、それに応じて、それぞれのメソッドを書き直す。
dataclass_jsonの登場
単一のインスタンスをserialize, desirializeするのが簡単になる。
https://lidatong.github.io/dataclasses-json/reference/dataclasses_json/api/
@dataclass_json
@dataclass
class Something:
のようにデコレータを追加するだけで
以下のメソッドが使える。
to_dict()
to_json()
from_dict()
from_json()
これらのメソッドを自分で書かなくて済むんだ。
これらのメソッドは、インスタンスを1つずつ扱っている場合には使いやすい。
import json
from dataclasses import dataclass
from dataclasses_json import dataclass_json, LetterCase
@dataclass_json(letter_case=LetterCase.CAMEL) # JSONプロパティ名をキャメルケースに変換
@dataclass
class Person:
name: str
age: int
persons = [
Person(name="Alice", age=30),
Person(name="Bob", age=25),
]
with open("person_dataclass.json", "wt") as f:
f.write(persons[0].to_json())
with open("person_dataclass.json", "rt") as f:
person_a = Person.from_json(f.read())
print(person_a)
# JSONファイルに直接書き込む
with open('persons_dataclass.json', 'w') as f:
f.write(f'[{",".join([person.to_json() for person in persons])}]')
[{"name": "Alice", "age": 30},{"name": "Bob", "age": 25}]
dataclass_jsonでのむずかしいところ
jsonファイルで保存できるデータ形式は、基本、数値と文字列、それらを用いた、辞書型とリストに限られてしまっていた。
classのデータメンバーに別のclassのインスタンスが使われているような状況は苦手だ。
pydantic
pydantic の登場以前は,入れ子になったデータ構造は、扱いが難しかった。
しかし、pydanticを使えば、それが簡単に扱える。
以下の例ではPersonクラスのインスタンスをEmployeeクラスのデータメンバーに持つ例だ。
書き込んだ結果を、簡単なコードで復元できているのがすごい。
from pydantic import BaseModel
import json
from pathlib import Path
from typing import List
DEFAULT_ENCODING = "utf-8"
class Person(BaseModel):
name: str
age: int
def persons_to_json(persons: List[Person]) -> str:
return json.dumps(persons, ensure_ascii=False, default=lambda o: o.model_dump())
def load_persons(name: Path) -> List[Person]:
with name.open("rt", encoding=DEFAULT_ENCODING) as f:
data = json.load(f)
return [Person(**person_data) for person_data in data]
class Employee(BaseModel):
person: Person
position: str
def employees_to_json(employees: List[Employee]) -> str:
return json.dumps(employees, ensure_ascii=False, default=lambda o: o.model_dump())
def load_employees(name: Path) -> List[Employee]:
with name.open("rt", encoding=DEFAULT_ENCODING) as f:
data = json.load(f)
return [Employee(**employee_data) for employee_data in data]
persons = [
Person(name="田中", age=30),
Person(name="佐藤", age=25),
]
with Path("persons.json").open("wt", encoding=DEFAULT_ENCODING) as f:
f.write(persons_to_json(persons))
persons = load_persons(Path("persons.json"))
for person in persons:
print(person)
employee = Employee(person=Person(name="Alice", age=30), position="programmer")
json_str = employee.model_dump_json()
with Path("employee.json").open("wt", encoding=DEFAULT_ENCODING) as f:
f.write(json_str)
employees = [
Employee(person=persons[0], position="programmer"),
Employee(person=persons[1], position="team_manager"),
]
with Path("employees.json").open("wt", encoding=DEFAULT_ENCODING) as f:
f.write(employees_to_json(employees))
reloaded_employees = load_employees(Path("employees.json"))
print(reloaded_employees)
[{"person": {"name": "田中", "age": 30}, "position": "programmer"}, {"person": {"name": "佐藤", "age": 25}, "position": "team_manager"}]
文字コードを指定すること、ensure_ascii=False とすることが、読みやすいjsonファイルにすることにつながる。
PyPI
追記
データ構造が複雑になればなるほど、pydantic での型検証が失敗しやすくなります。
既存コードでの型チェックの重要性が高まります。
mypy などを使って型チェックの検証をしていくことです。
追記
Python3.9以降をお使いの方は
def persons_to_json(persons: List[Person]) -> str:
return json.dumps(persons, ensure_ascii=False, default=lambda o: o.model_dump())
typing.Listではなくlistに置き換えてください。
def persons_to_json(persons: list[Person]) -> str:
return json.dumps(persons, ensure_ascii=False, default=lambda o: o.model_dump())
以下の記事を参考にしました。感謝
dataclassを捨ててpydanticに乗り換える
ネストされたdictやjsonからdataclassを作成したいならpydanticが便利