dataclassの型ヒントを強制できるpydanticがとても便利だった話

どうも、きっしゅです。

Python3.7から正式にリリースされた dataclass は非常に便利で使っている方も多いと思います。

dataclass は値を管理することに優れていて、かつ型ヒントも記載できるので可読性の向上にも貢献します。

ただこの型ヒントはあくまでヒントであって、記載されている型と異なる型のデータを格納しようとしても一切エラーは発生しません。

この型の定義、強制がないのは Python のいい面ではあるものの、コードを書く時は型を強制したいって考えている人は少なくはないと思います。

そんな悩みを解消してくれるのが、Python の外部パッケージである pydantic です。

個人的にはとても便利だと感じたので個人的なメモも含めて記事しました。

pydanticが便利だった話

そもそもpydanticとはなにか?

まず pydantic ってなになのか。pydantic の公式の記載を簡単に引用すると下記の通りです。

  • 定義された型ヒントを使ってデータのバリデーションやデータの管理を行ってくれる
  • 型ヒントを強制して異なる型が渡された時は、型変換やユーザーフレンドリーなエラーを出力してくれる

ざっくりとこんな感じです。

要するに型ヒントを使っていい感じにそれぞれのデータを扱ってくれるってことですね(めっちゃ雑。笑)

百聞は一見にしかず、ってことで実際にコードを書いて検証して行きます。

pydanticのインストール

まずは pydantic をインストールしないと始まらないのでインストールします。

インストールは他のライブラリと変わらず pip で可能です。

pip install pydantic

これでインストールは完了ですので、実際にコードを書いて行きます。

型ヒントを強制する

まずは型ヒントの強制に関してです。

まず通常の dataclass を使った実装です。

from dataclasses import dataclass
from datetime import date


@dataclass
class NoPydantic:
    name: str
    age: int
    birthday: date


d = date(2000, 7, 7)
sample = NoPydantic(name='User Name', age=21, birthday=d)

print(sample)
print(f'Type of name => {type(sample.name)}')
print(f'Type of age => {type(sample.age)}')
print(f'Type of birthday => {type(sample.birthday)}')

----- 出力結果 -----
NoPydantic(name='User Name', age=21, birthday=datetime.date(2000, 7, 7))
Type of name => <class 'str'>
Type of age => <class 'int'>
Type of birthday => <class 'datetime.date'>

まあ正しい値を入れているので想定通りの結果です。

一方で型ヒントと異なる値を渡した場合は下記のようになります。

from dataclasses import dataclass
from datetime import date


@dataclass
class NoPydantic:
    name: str
    age: int
    birthday: date


dt = datetime(2000, 7, 7)
sample2 = NoPydantic(name=12345, age='HogeHoge', birthday=dt)

print(sample2)
print(f'Type of name => {type(sample2.name)}')
print(f'Type of age => {type(sample2.age)}')
print(f'Type of birthday => {type(sample2.birthday)}')

----- 出力結果 -----
NoPydantic(name=12345, age='HogeHoge', birthday=datetime.datetime(2000, 7, 7, 0, 0))
Type of name => <class 'int'>
Type of age => <class 'str'>
Type of birthday => <class 'datetime.datetime'>

エラーが発生することなく、渡された型がそのまま入ってしまっています。

これだと型ヒントを信じて実装をするとどこかでエラーが発生する可能性があります。

__post_init__ を使ってバリデーションを実装するのは1つの手段ですが、実際めんどくさいと思います。

ここで本日の主役である pydantic が登場します。

全く同じ実装を pydantic を使ってやってみます。

使い方としては pydantic の中に dataclass があるのでそれを使えばOKです。

なのでインポート文は下記の通りです。

from pydantic.dataclasses import dataclass

それを踏まえて先程の実装をすると下記の通りです。

from pydantic.dataclasses import dataclass
from datetime import date, datetime


@dataclass
class UsePydantic:
    name: str
    age: int
    birthday: date


d = date(2000, 7, 7)
sample = UsePydantic(name='User Name', age=21, birthday=d)

print(sample)
print(f'Type of name => {type(sample.name)}')
print(f'Type of age => {type(sample.age)}')
print(f'Type of birthday => {type(sample.birthday)}')

----- 出力結果 -----
UsePydantic(name='User Name', age=21, birthday=datetime.date(2000, 7, 7))
Type of name => <class 'str'>
Type of age => <class 'int'>
Type of birthday => <class 'datetime.date'>

まあここに関しては正しい値を入れているため先ほどと結果は同じです。

一方で型ヒントと異なる値を渡した場合は下記の通りです。

from pydantic.dataclasses import dataclass
from datetime import date, datetime


@dataclass
class UsePydantic:
    name: str
    age: int
    birthday: date


dt = datetime(2000, 7, 7)
sample2 = UsePydantic(name=12345, age='HogeHoge', birthday=dt)

print(sample2)
print(f'Type of name => {type(sample2.name)}')
print(f'Type of age => {type(sample2.age)}')
print(f'Type of birthday => {type(sample2.birthday)}')

----- 出力結果 -----
Traceback (most recent call last):
  line 22, in <module>
  line 6, in __init__
  File "...... /python3.9/site-packages/pydantic/dataclasses.py", line 99, in _pydantic_post_init
    raise validation_error
pydantic.error_wrappers.ValidationError: 1 validation error for UsePydantic
age
  value is not a valid integer (type=type_error.integer)

出力結果は見ての通り、ValidationError です。

今回の場合はログに記載のあるとおり、ageint 型ではない値が渡されたためエラーが発生しています。

これが pydantic によるバリデーションです。pydantic を使うことによって型チェックのバリデーションを独自実装する必要がなくなりました。

どの変数の値がダメだったのかも出力してくれるので原因の特定とコードの修正も簡単です。

型変換をしてくれる

pydantic のもう一つの特徴に必要に応じて型変換を自動で行なってくれるという点があります。

当然ながら限界はあるのですが、これもまた非常に便利です。

実際にコードを書いて確認してみます。

from pydantic.dataclasses import dataclass
from datetime import date, datetime


@dataclass
class UsePydantic:
    name: str
    age: int
    birthday: date

dt = datetime(2000, 7, 7)
sample4 = UsePydantic(name=12345, age='21', birthday='2000-07-07')

print(sample4)
print(f'Type of name => {type(sample4.name)}')
print(f'Type of age => {type(sample4.age)}')
print(f'Type of birthday => {type(sample4.birthday)}')


----- 出力結果 -----
UsePydantic(name='12345', age=21, birthday=datetime.date(2000, 7, 7))
Type of name => <class 'str'>
Type of age => <class 'int'>
Type of birthday => <class 'datetime.date'>

上記のコードではクラスインスタンスの作成時に渡している引数のデータ型が実際にクラスで定義されている型ヒントとは異なっています。

namestr を期待しているが、int を渡しています。

ageint を期待しているが、str を渡しています。

birthdaydatetime.date を期待していますが、str を渡しています。

バリデーション的にはエラーになりそうですが、実際の結果は出力結果にあるようにエラーは発生しません。

それどころが、それぞれのデータは型ヒントと一致するようにデータ型が変わっています。

これが pydantic のもう一つの特徴である、型ヒントに合わせたデータ型の変換です。

当然ながら型変換といっても限界はあるので、バリデーションの時の例で示したような int を期待しているとことに hogehoge という数字への変換が不可能なデータを渡すとバリデーションエラーが発生します。

外部からのデータを扱う際は微妙なデータ型のズレはあるっちゃあるので、少々の想定外にも勝手に対応してくれるのはありがたいと思います。

まとめ

いかがでしたでしょうか?

今回は pydantic の型ヒントの強制と型変換に関して触れました。

動的型付け言語である Python にとって型強制は Python らしくないって言ったら、まあそうだとは思います。

けど pydantic を使うことにで、型ヒントを書くだけで、型安全性、可読性、保守性が高まるって考えると、個人的にはメリットの方が多いと感じたので今後も使っていきたいなと思いました。

実際にはもっといろんな機能があるみたいなので興味深いものがあったらまた記事にしようと思います。

みなさまの役に立ちますと幸いです。

Python
きっしゅをフォローする
Ibukish Lab+

コメント

タイトルとURLをコピーしました