PythonでJSONデータを扱う工夫

これは TECHSCORE Advent Calendar 2019 の16日目の記事です。

PythonでJSONデータを扱う

最近ではマイクロサービスだなんだと、外部サービスのHTTPのAPIを呼び出し、JSONデータを扱う機会は少なくないと思います。

Pythonではjsonパッケージを利用してdict(辞書)に変換して利用することが多いのではないでしょうか。

dictの要素を取得する場合には、getを使用するとキーが存在しなかった場合でもKeyErrorが発生せず、指定の値を返すことができます。なので個人的にはgetを好んで使っています。

階層が深いプロパティにアクセスする場合、以下のようになります。dictがネストしていく構造になっています。

ブラケットにしてもgetにしても、シンプルなデータであれば問題にはなりませんが、階層が深くなると冗長な感じになります。

objectっぽくアクセスしたい

ブラケットやgetを使ったアクセスではなく、オブジェクトと属性のような .(dot) を使用してデータを利用したいです。先ほどの例だと以下のような使い方になります。

これは実際よくある話っぽく、ググると色々とやり方が出てきます。

例えば、dictを継承して属性が有るかのように振る舞うクラスを定義する方法があります。

シンプルにアクセスできるようになりましたね。

jsonパッケージと一緒に使う

この ObjectLike をJSONの読み取りの際にも利用します。

json.loads には object_hookという引数があり、JSONのObjectを処理する際のフックを仕込むことができるようになっています。dict を受け取って何らかの値を返す関数であれば良いので ObjectLike をそのまま使用することができます。

良い感じになってきました。

JSON側のCamelCaseに対応する

JSONのキー名では CamelCase (あるいはmixedCase)になっていることもままあります。一方で、Pythonのコーディングにおいて属性名は snake_case が推奨されています(関数や変数の名前 — pep8-ja 1.0 ドキュメント)。

ObjectLike の属性としては snake_case でアクセスできるようにしたいですね。属性アクセスの際に都度 case の確認をするのは嬉しくないので、JSONの処理時にキーを全て snake_case に変換するようにしてみます。実装は雑ですが、こんな感じになります。

元のJSONキーは zipCode ですが、変換したので user.home.zip_code でデータが取得できるようになりました。 逆に user.home.zipCode ではキーが存在しないため None が返ってくるようになっています。

これでけっこう現実的に使える範囲になった気がします。

課題:途中の属性がないとエラーになる

JSONデータをよしなに扱えるようになってきたのですが、まだ解決できていない問題があります。
例えば間違った属性へアクセスをしてしまった場合や、そもそもデータがなかった場合など、チェインしている途中の属性が欠落してしまうとエラーになります。(APIの仕様によっては属性があったりなかったりしますよね?

これは user.home が None であり None に対して city という属性がないので発生します。

回避策がないことはないのですが…

getのデフォルト値を指定する

ObjectLike が間接的に呼び出している dict.get ですが、デフォルト値が指定されていないので None が返っています。これを空のObjectLikeを返すようにしてみます。

AttributeError は起きなくなりましたが None は返ってきません。

try/except で囲む

愚直にやるのが一番!?とも思いますが、アクセスするたびに try/except で囲むのはちょっとめんどくさいですね。

try/except で囲む 2

途中の属性がないのでエラーになるというのは構造が想定したものと異なる状況なのでそのままエラーとして扱えた方が良いでしょう。

またdict.getのデフォルト値はNoneのままとして or を使ったデフォルト値を指定してみます。

Optional Chainingができると良いんだけどなー。

課題:dictの属性とかぶるJSONのキーは利用できない

__getattr__ は属性がない場合に実行されるので、dictに存在する属性はそのまま利用できます。なので、名前がかぶるとJSONのデータが利用できなくなります。

例えば values というのは辞書の値だけを取得するメソッドです。名前がかぶるので user.values は配列ではなくメソッドそのものが取得できます。実行すると配列が取得できますが、辞書全体の値の配列です。ここでは[1,2,3,4,5] が欲しいので、意図しない動作になっています。

まとめ

実際のところpython-boxといったライブラリもあるので、そちらを利用した方が良いと思いますが、とりあえずObjectLikeの仕組みでJSONのデータがそこそこ良い感じに扱えるのが確認できました。

また Python3.8 からは TypedDict というType Hintingのための新しい型が導入されているので、それを活用する方法もありそうです。

あと、Goで使える Golang: Convert JSON to Struct のPython版が欲しかったのですが、見当たらなかったのでした。

おわり

Comments are closed, but you can leave a trackback: Trackback URL.