【Python】プリミティブな型をちょっとリッチに拡張する

きっかけ

今の案件で基本的にはintの振る舞いで構わないんだけど、 その数値の持つ意味をコードに反映したいということになった。

たとえば、型で具体的に何の値を指しているのかをアノテーションをしたり、ちょっとした変換処理やバリデーションや制約を数値型に実装したり、メソッドを生やしたり。

基本的にはintの振る舞いで構わないというのがミソ

なのでがっつりクラスを作るようなことはしたくなかった。

組み込みライブラリを使う

typingライブラリのNewTypeはintやstrといったプリミティブな型に名前をつけることができる。

from typing import NewType

UnixTime = NewType("UnixTime", int) 

これで、UnixTimeという名前でintと全く同じ振る舞いをする型を生成できる。

型で具体的に何の値を指しているのかをアノテーションするのに役立つ。

バリデーションや制約をつける

上記のNewTypeはバリデーションとかができない。

じゃあどうするか

Pythonのintやstrは中身はclassである。

なので継承して、新たな型を作りだすことが可能

たとえば、

from datetime import datetime

class UnixTime(int):

    def __new__(cls, value):
        # vlaueをゴニョゴニョする処理
        ...
        
        return super().__new__(cls, value)
    
    def to_datetime(self) -> datetime:
        return datetime.fromtimestamp(self)

これでinstance化の時にバリデーションしたり、その付加した意味に適したメソッドを生やすことができる。

__new__を使っているのは、intやstrなどの不変な型(≒プリミティブっぽいやつ)を作成するには__init__は遅すぎるため。

__new__はオブジェクトを生成する前に実行される。