7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

pydanticを用いて堅牢な型を作成する

Last updated at Posted at 2022-07-29

概要

堅牢な型(Strict Type)が現時点で5つしかなく、結局__post_init__などでバリデーションを行う必要があるのは面倒と感じていましたが、なんとpydanticでは型を自分で作ることが出来ます!

ただの型の作成であれば、下記のQiita記事やドキュメントを参照すれば作ることが出来ます。

今回はこれの堅牢な型バージョンです。

型の作成方法

めちゃくちゃ簡単です。

  1. 作成する型のクラスを定義する
  2. __get_validators__を定義して、次で作成するvalidate関数を呼び出す
  3. 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を学んで有意性を示して取り入れていきたいですね。

  1. isinstanceはサブクラスの時もTrueを返すので、isinstance(True, int)などで正しい値を返さない。

7
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?