概要
堅牢な型(Strict Type)が現時点で5つしかなく、結局__post_init__
などでバリデーションを行う必要があるのは面倒と感じていましたが、なんとpydantic
では型を自分で作ることが出来ます!
ただの型の作成であれば、下記のQiita記事やドキュメントを参照すれば作ることが出来ます。
今回はこれの堅牢な型バージョンです。
型の作成方法
めちゃくちゃ簡単です。
- 作成する型のクラスを定義する
-
__get_validators__
を定義して、次で作成するvalidate
関数を呼び出す -
validate
を定義する関数を作成し、エラーパターンを記述する
簡単な例を紹介すると、0以外の数値を受け取るNotZeroNumber
型を定義してみます。
from pydantic.dataclasses import dataclass
class NotZeroNumber(int):
@classmethod
def __get_validators__(cls):
yield cls.validate
@classmethod
def validate(cls, num):
# 文字列を弾く
try:
num = int(num)
except:
raise TypeError("num is not number")
# ゼロを弾く
if num == 0:
raise ValueError("num is Zero")
return num
@dataclass
class NumberManager:
num: NotZeroNumber
def Output(self) -> None:
print(f"{self.num} : {type(self.num)}")
def main():
# OK -> " 1 : <class 'int'> "
NumberManager(1).Output()
NumberManager(1.0).Output()
NumberManager("1").Output()
NumberManager(True).Output()
# NG
# NumberManager(0).Output() # num is Zero (type=value_error)
# NumberManager(0.0).Output() # num is Zero (type=value_error)
# NumberManager("0").Output() # num is Zero (type=value_error)
# NumberManager("a").Output() # num is not number (type=type_error)
# NumberManager(False).Output() # num is Zero (type=value_error)
if __name__ == "__main__":
main()
このようにさっくりと書くことが出来ます。
int
型を継承しているので、int
型と同じ振る舞いが可能です。
validate関数について
今回は全てvalidate
関数に定義しましたが、同じ条件を何度も使うのであれば関数に切り出してOKです!
def IntValidate(num):
try:
return int(num)
except:
raise TypeError("num is not number")
def NotZeroValidate(num):
if num != 0:
return num
raise ValueError("num is Zero")
class NotZeroNumber(int):
@classmethod
def __get_validators__(cls):
yield IntValidate
yield NotZeroValidate
同じ処理を何度も書かなくて済むのがめちゃくちゃ楽!
堅牢な型の作成方法
validate
関数に、"その"型かどうかを確認する処理を入れるだけです。
(堅牢なint
型を作るのであれば、入力値がint
型か確認するだけ)
-> type(入力値) is int
で確認できる。1
例として、datetime.date
型しか受け取らない型を定義してみましょう。
from pydantic.dataclasses import dataclass
import datetime
class StrictDatetimeDate(datetime.date):
@classmethod
def __get_validators__(cls):
yield cls.validate
@classmethod
def validate(cls, date):
if type(date) is not datetime.date:
raise TypeError("not datetime.date type")
return date
@dataclass
class DateRecorder:
strict_date: StrictDatetimeDate
def Output(self):
print(type(self.strict_date))
def main():
# OK
DateRecorder(datetime.date.today()).Output() # <class 'datetime.date'>
# NG
DateRecorder(datetime.datetime.now()).Output() # not datetime.date type (type=type_error)
if __name__ == "__main__":
main()
datetime.date
型しか受け取らない型を定義することが出来ました!
datetime.date
型を継承しているので、strict_date.year
などdatetime.date
がサポートしているメソッドを使用することが出来ます。
因みに型を作らずに下記のような書き方をすると、上記でNGが出たdatetime.datetime
型はエラーにならずキャストされてしまいます。
from pydantic.dataclasses import dataclass
import datetime
@dataclass
class DateRecorder:
date: datetime.date
def Output(self):
print(type(self.date))
def main():
DateRecorder(datetime.date.today()).Output() # <class 'datetime.date'>
DateRecorder(datetime.datetime.now()).Output() # <class 'datetime.date'>
if __name__ == "__main__":
main()
終わりに
こんな簡単に型が書けるのはすごい!そして型書くのめちゃくちゃ楽しい!
是非皆さんも書いてみてください!
(pydantic
の使い方やインストール方法は、前回の記事に書いてあるのでぜひ…!)
おまけ
堅牢な型の小話
上述していますが、pydantic
を用いると異なる型を渡した際は、クラスで定義した型にキャストされます。
定義した型にキャストされるのであれば、別にそこまで堅牢じゃなくてもいいんじゃない?って思う方もいるかもしれません。
しかし下記のようなコードがあったら違和感を感じませんか?
from pydantic.dataclasses import dataclass
@dataclass
class User:
age: int
def IncreaseAge(self) -> None:
"""年齢を+1する"""
self.age += 1
user_info = User("20")
- なぜクラス側は
int
で、呼び出し側はstr
なのか- しかもエラーを出さずに動く
-
IncreaseAge
は、21
なのか201
なのか実行しないと確信が持てない(特にpydantic
を知らない方は)
確かに仕様上は動く。でも、これは優しいコードですか?正しいコードですか?
自分はこれが正しいコードとは思いません。
キャストされるとしても、呼び出し側ではクラス側と同じint
に合わせるべきだと思っています。
チーム開発をしていて付きまとうのは、今後の改修で上記のように誤って異なる型を渡す書き方をする方が現れるかもしれません。
そしてコードが仕様通りに動いて気付かずにレビューが通ってしまうかもしれません。
そういうことを防ぐのが、今回まとめた堅牢な型の役割です。
堅牢な型を使うことで上記のような型違いはキャストされずにエラーで落とします。
(今回のような絶対int
型でしか渡されたくない場合は、pydantic
が用意しているStrictInt
を用いることが出来ます)
まだ弊社では取り入れていませんが、もっとpydantic
を学んで有意性を示して取り入れていきたいですね。
-
isinstance
はサブクラスの時もTrue
を返すので、isinstance(True, int)
などで正しい値を返さない。 ↩