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

## 概要  

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

ただの型の作成であれば、下記のQiita記事や[ドキュメント](https://pydantic-docs.helpmanual.io/usage/types/#custom-data-types)を参照すれば作ることが出来ます。 
 
https://qiita.com/kikuchi-yzrh/items/175d6dfd01fd2a3db17a  
  
今回はこれの堅牢な型バージョンです。  
  
## 型の作成方法  
めちゃくちゃ簡単です。  
1. 作成する型のクラスを定義する  
1. `__get_validators__`を定義して、次で作成する`validate`関数を呼び出す  
1. `validate`を定義する関数を作成し、エラーパターンを記述する  

簡単な例を紹介すると、0以外の数値を受け取る`NotZeroNumber`型を定義してみます。  
```python
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`型と同じ振る舞いが可能です。  

<details><summary>validate関数について</summary>  

  
今回は全て`validate`関数に定義しましたが、同じ条件を何度も使うのであれば関数に切り出してOKです！  
```python
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

```

同じ処理を何度も書かなくて済むのがめちゃくちゃ楽！    

</details>

## 堅牢な型の作成方法  
`validate`関数に、"その"型かどうかを確認する処理を入れるだけです。  
(堅牢な`int`型を作るのであれば、入力値が`int`型か確認するだけ)  
-> `type(入力値) is int`で確認できる。[^1]  


例として、`datetime.date`型しか受け取らない型を定義してみましょう。  
```python
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`型はエラーにならずキャストされてしまいます。  
```python
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`の使い方やインストール方法は、前回の記事に書いてあるのでぜひ…！)  


### おまけ
<details><summary>堅牢な型の小話</summary>

上述していますが、`pydantic`を用いると異なる型を渡した際は、**クラスで定義した型にキャストされます**。
定義した型にキャストされるのであれば、別にそこまで堅牢じゃなくてもいいんじゃない？って思う方もいるかもしれません。  
  
しかし下記のようなコードがあったら違和感を感じませんか？  
```python
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`を学んで有意性を示して取り入れていきたいですね。  

</details>

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