概要
@dataclass
を用いると型アノテーションが強制化されますが、この型自体に強制力はありません1。
例えば、下記のように違う型を入れた場合でも動作してしまいます。
from dataclasses import dataclass
@dataclass
class User:
name: str
age: int
def main():
user_info = User(20, "Hoge")
print(user_info) # User(name=20, age='Hoge')
print(type(user_info.name)) # <class 'int'>
print(type(user_info.age)) # <class 'str'>
if __name__ == "__main__":
main()
なぜ強制力がないかというと、この型アノテーションは型のヒントでしかなく、言うのであればコメントと同様です2。
仕様だとしても、せっかく定義した型が違う型に書き換わるのは解せない…
そういう方におすすめなのがpydanticです!
型アノテーションの補足
強制力がないならばあまり書く意味がないと思われますが、主に下記のメリットが存在します。
- 他の開発者や未来の自分に、この変数の型は何が入るべきなのかを示すことが出来る
- コードの読み解きが楽になる
-
mypy
やPylance
と合わせると型チェックを行ってくれて、開発しながら問題のある部分が明白になる
注意点として、Python3.5以上でなければ使用できない点です。
なので、Python3.5以上であればバンバン使っていくことをおすすめします。
(関数の返り値の型まで書くことが出来ます!)
pydanticとは
ドキュメントには下記のように記載されています。
- pydanticは実行時に型ヒントを強制し、データが無効な場合はユーザーフレンドリーなエラーを提供します
- 純粋で標準的なpythonでデータがどのようにあるべきかを定義し、pydanticでそれを検証します
と書いてありますが、この記事内では型を強制させる代物という理解で問題ありません。
インストール
外部モジュールなので、下記コマンドを実行しインストールする必要があります。
pip install pydantic
使い方
from dataclasses
からfrom pydantic.dataclasses
に変更するだけで使用できます!
from pydantic.dataclasses import dataclass
@dataclass
class User:
name: str
age: int
def main():
user_info = User("Hoge", 20)
print(user_info) # User(name='Hoge', age=20)
print(type(user_info.name)) # <class 'str'>
print(type(user_info.age)) # <class 'int'>
if __name__ == "__main__":
main()
今まで使用してきた@dataclass
と同じですね!
エラー送出機能と型変換
ここからがpydantic
の本領発揮です。
下記のように定義した型と異なる型を受け取った際には、エラーを出力してくれます。
from pydantic.dataclasses import dataclass
@dataclass
class User:
age: int
def main():
user_info = User("Hoge")
print(user_info)
print(type(user_info.age))
if __name__ == "__main__":
main()
実行すると下記のようなエラーが送出されます。
Traceback (most recent call last):
File "", line 17, in <module>
main()
File "", line 10, in main
user_info = User("Hoge")
File "<string>", line 4, in __init__
File "pydantic\dataclasses.py", line 100, in pydantic.dataclasses._generate_pydantic_post_init._pydantic_post_init
# +-------+-------+-------+
pydantic.error_wrappers.ValidationError: 1 validation error for User
age
value is not a valid integer (type=type_error.integer)
このようにpydantic
を用いることで、定義した型と異なる型が与えられた時にはエラーを出力してくれます。
……しかし、このようにエラーを出してくれるのはキャストが出来ないときに限られます。
試しにキャストが出来る文字列型の"20"をage
に格納してみましょう。
from pydantic.dataclasses import dataclass
@dataclass
class User:
age: int
def main():
user_info = User("20")
print(user_info) # User(age=20)
print(type(user_info.age)) # <class 'int'>
if __name__ == "__main__":
main()
このようにstr
型の文字列が、定義したint
型にキャストされて受け取っています。
文字列だけではなく、float
型であれ、bool
型であれ、キャスト出来るならキャストをしてしまいます。
これはpydantic
の仕様です(バグではありません)3。
キャストで型変換出来る場合でも、キャストをせずにエラーを出して欲しい。
そんな要望にもpydantic
は応えてくれます!4
Strict Types
この型で定義すると、キャストでの変換を防いでエラーを出してくれます。
種類としてはStrictStr
, StrictBytes
, StrictInt
, StrictFloat
, StrictBool
があります。
使い方は、from pydantic
から使用する型をimportして、型アノテーションで定義するだけです。
from pydantic.dataclasses import dataclass
from pydantic import StrictInt
@dataclass
class User:
age: StrictInt
def main():
# ValidationError
# user_info = User(3.14)
# user_info = User("3")
# user_info = User(True)
# OK
user_info = User(3)
print(user_info) # User(name=3)
print(type(user_info.age)) # <class 'int'>
if __name__ == "__main__":
main()
キャスト出来ていたものが軒並みValidation Error
になり、"真のint
型"のみ受け取るように強制させることが出来ました!
他のStrictStr
やStrictFloat
なども同様の動作などで省きます。5
ちなみに型を自分で作ることが出来るので、datetime.datetime
型しか受け取らない型を作るなんてことも可能です!
終わりに
今回は、pydantic
を使って@dataclass
の型を堅牢にすることに絞ってまとめてみました。
ここで使用した型は一部分で、pydantic
は様々な型をサポートしています(参照)
また思った以上にpydantic
は奥深く、issueやドキュメントを読んでいるだけでも面白かったのでおすすめです6。
おまけ
キャストのタイミングについて
引数で値を受け取ったと同時にキャストしているのか、と思っていたがどうやら__post_init__
の終了後にキャストしてるっぽい。
from pydantic.dataclasses import dataclass
@dataclass
class User:
age: int
def __post_init__(self):
print(f"{self.age=}")
print(f"Type = {type(self.age)}")
def OutputType(self):
print(f"OutputType : {type(self.age)}")
def main():
user_info = User("20")
user_info.OutputType()
if __name__ == "__main__":
main()
出力は下記の通りです。
self.age='20'
Type = <class 'str'>
OutputType : <class 'int'>
ちょっとびっくりした小話でした。
参考文献