Python の型ヒントで JSON の型チェックをする
Posted on 18 December 2018.型、つけてますか?Python の関数にアノテーションを付与するための構文が導入されて以来、型情報の付与について着々と規約・ツールが整備されてきました。 こういった最近の流れをみると、やはり人類は JSON はじめとする型のゆるいデータに対する型チェックをうまくやりたくなるというものです。 そういうわけで、今回は Proof of Concept 的に typedjson という小さなライブラリを実装してみたので、歴史の話も少し混ぜつつ紹介します。
アノテーションの構文の導入から型ヒントまで
動的型付け言語である Python においても、3.0.x から現在最新である 3.7.x になるまで、型の情報を付与するための規約などの整備がおこなわれてきました。 特に関連のありそうなものをあげると、ざっくり以下のような PEP が存在します。
- PEP 3107 – Function Annotations: 関数の引数・結果に対するアノテーション構文の導入
- PEP 484 – Type Hints: アノテーションを型情報の付与のために使用・標準モジュール
typing
提供・各種定義標準化 - PEP 526 – Syntax for Variable Annotations: 変数・フィールドへのアノテーション構文の導入
- PEP 544 – Protocols: Structural subtyping (static duck typing): より Pythonic な structural subtyping の導入
- PEP 563 – Postponed Evaluation of Annotations: 型ヒント前方参照問題の解消
PEP では、このように Python のコードに型情報を付与するための構文の整備やその解釈の取り決めが進んできました。 一方、実際のそのアノテーションの使用 (例えば静的型検査をする・ドキュメントを生成する) については、mypy などのサードパーティのツールに任されています。
Python で静的型検査をする
ここまでに列挙したように、Python 本体としては、アノテーションのための構文の導入に続き、着々と型情報を付与するためのモジュールやその解釈の標準化を進めてきました。 そうした規約に則って型情報を付与された Python のコードは mypy というツールを使用することで、静的に型検査をすることができます。 下は簡単な例ですが、どちらも mypy 公式ページに上げている fibonacci 数列を返す関数を使用したコードです。 順に型情報を付与していない版・型情報付与した版になります。
どちらも実行するとランタイムエラーになりますが、型情報を付与した後者のみ mypy を使用すれば実行前にエラーを検出できます。 よりコードが複雑になってくると、一度実行するだけでエラーを検出するのはきわめて困難です。よって実行前に検出できるエラーが増えることは品質の上でも大きなメリットです。
動的型付けから静的型付けへの移行と考えるとすごく大変そうでありますが、mypy には安心できる点がかなりあります。
- 実行時に影響を与えるツールではなく、型ヒント自体も実行に原則影響を与えないので、万が一の場合も捨てやすい
--strict
を使用すれば厳格な型チェックができる一方、少しづつ型情報を付与するために untyped な宣言を許すこともできる- PEP 484 より先に登場したツールだが、PEP 484 に準拠している (独自路線ではなく標準的なツールとみなせる)
- 既存コード・ライブラリについても、stubgen をはじめとする、型情報を付与する作業を簡略化するツールがある
PEP 484 や PEP 544 などで定義されている型システムはなかなかリッチで、ジェネリクスはもちろん、Optional
もありますし、Protocol
を使用して従来の Python らしさは維持しつつ静的型チェックをすることもできます。
また、現在の型システムでは、既存コードの実装上で扱うのが難しいケースのためにプラグイン機構の実装も進められているようです。
例としては、dropbox/sqlalchemy-stubs や、numpy に関連する話題 があります。
型ヒントを使用して JSON を型チェックする
以上のような Python を取り巻く近況があり、「JSON の型チェックも型ヒントを引き回せばいい感じにできて、型情報が付与されたコードを活かせるのでは?」と思った次第です。 そこで先日、「Python の型ヒントで JSON の型チェックをする」というタイトルで LT をしていました。
このスライドの終盤で話している typedjson を使用すると、以下のことが可能です。
- JSON (正確には
json.load
で戻された辞書) を data class (PEP 557 を参照) に型チェックをした上で変換できます - 型チェックのための退屈なロジックを組む必要がありません、変換先の型ヒントでチェックされます
Optional
をはじめとするUnion
や、homogeneous / heterogeneous なTuple
への変換も可能です
つまり、mypy を使用していると必ず書くであろう型ヒントを書くだけで、JSON の実行時における型検査のコードが書けてしまい、その先は mypy の静的型検査の恩恵を受けることができるというわけです (mypy を使用してしていなくとも今後は data class の使用が増えていくと思われますが)。
使用例としては、以下の通りです。型に着目した上で CatJson
に変換可能である辞書のみが変換でき、変換不可能な辞書に対しては、問題となったフィールドのパス付きで DecodingError
を返しています。
もちろん、現状では以下の通りいくつか制限・懸念事項はありますが、今後可能な範囲で解消していく予定です。
- PEP 563 前提、つまり
from __fuiture__ import annotations
が必須です。 (Python 3.7 以上) - クラスインスタンスへの変換は、非ジェネリックかつ
__init__
をユーザーが定義していない data class のみ対応してます - 型チェックのために使用している
__origin__
や__args__
が undocumented な API である (消えても代替 API がほぼ確実に出るという話はあるが…)
なお、typedjson は、すでに PyPI にも公開しているので pip
からインストールして試してみることが可能です。
とりあえず出来そうなのでやってみたという現状なので、pull-req などなどお待ちしております。