Pythonのクラスでどうしても型チェックしたかった(できた)

Pythonのクラスでどうしても型チェックしたかった(できた)

前段: Pythonの型チェック

Pythonで型チェックをするとき、よくこういうプログラムを書くことが以前にありました。

val = 30
if type(val) is int:
    print('val is int')

でも、最近のPythonではisinstance()が主流なんですね。

if isinstance(val, int):
    print('val is int')

Pythonは型をチェックしないと、後々のバグの温床になってしまうので、だいたい関数の入力の型チェックをするのに書きます。

def add(a, b):
    if isinstance(a, int) and isinstance(b, int):
        return a + b
    else:
        raise TypeError('both of a and b are int.')


add(3, 5)           # -> 8
add('sam', 'ple')   # TypeError

「なんで、Pythonで型を固定したいんだ?」と言われたらそれまでなのですが、それでもライブラリーを開発するときにはやはり必要になるんじゃないかと思って、tipsとして書きます。

アノテーション

アノテーションというのはPython 3.5のときから導入されたものです。

こういうやつ。

val   : int  = 5    # int型
is_num: bool = True # bool型

val = 'TEXT'        # 型を縛った訳ではないのでエラーにならない

ただ単のアノテーションなので、C/C++みたいなエラーは出てきません。

同じように関数ではこのようにアノテーションします。

def add(a: int, b: int):
    return a + b

add(3, 5)           # -> 8
add('sam', 'ple')   # -> 'sample' エラーにはならない

これも同様にアノテーションしているだけなので、エラーは出てきません。

関数を用いた型チェック

最初の方に書いた例にアノテーションんを追加すると、こんな感じになります。

def add(a: int, b: int):
    if isinstance(a, int) and isinstance(b, int):
        return a + b
    else:
        raise TypeError('both of a and b are int.')


add(3, 5)           # -> 8
add('sam', 'ple')   # TypeError

でもこの中で毎回isinstance()って書くのは面倒だなって思い、改良してみた。

def type_check(**kwargs):
    for val_name, (val, type_) in kwargs.items():
        if not isinstance(val, type_):
            raise TypeError(f'{val_name} is illegal type. <{val=}, {type_=}>')


def add(a: int, b: int):
    type_check(a=(a, int), b=(b, int))
    return a + b


add(3, 5)     # -> 8
add(3, 'ple') # TypeError

いや、これも面倒だって

まあ、そりゃそうですよね。

なんでわざわざ(a, int)とか引数を渡さなきゃいけないんだってなります。

だからデコレーターでやります。

import inspect


def deco_type_check(fn):

    def inr_fn(*args, **kwargs):

        sig = inspect.signature(fn)

        for val_name, val in sig.bind(*args, **kwargs).arguments.items():

            annot = sig.parameters[val_name].annotation
            type_ = annot if isinstance(annot, type) else inspect._empty

            if type_ is not inspect._empty and not isinstance(val, type_):
                raise TypeError(f'{val_name} is illegal type. <{val=}, {type_=}>')

        return fn(*args, **kwargs)

    return inr_fn


@deco_type_check
def add(a: int, b: int):
    return a + b


add(3, 5)     # -> 8
add(3, 'ple') # TypeError

上の方のデコレーター用の関数はツラいですが、コピペして使っても問題ないので、これを使えば、関数での型を固定することができます。

本題: クラスでの型チェック

これ、デコレーターでやろうかどうか迷ったのですが、どうしてもうまく行かなかったので、クラスの継承を使うことにしました。

import inspect


class Expression(object):

    def __init__(self, *args, **kwargs):

        sig = inspect.signature(self.__init__)

        for k, v in sig.bind(*args, **kwargs).arguments.items():
            annot = sig.parameters[k].annotation
            t = annot if isinstance(annot, type) else inspect._empty
            if t is not inspect._empty and not isinstance(v, t):
                raise TypeError('{k} must be {tname}, not {vcls_name}.'.format(
                        k        =k,
                        tname    =t.__name__,
                        vcls_name=v.__class__.__name__
                    ))

            self.__dict__[k] = v


    def __setattr__(self, k, v):

        if k not in self.__dict__ type(v) == type(self.__dict__[k]):
            self.__dict__[k] = v

        else:
            raise TypeError('{k} muse be {tname}, not {vcls_name}'.format(
                   k        =k,
                   tname    =self.__dict__[k].__class__.__name__,
                   vcls_name=v.__class__.__name__
                ))



class Cls(Expression):

    def __init__(self, name: str, age: int):

        super().__init__(name, age)



if __name__ == '__main__':

    cls = Cls('Alice', 20)
    cls.age = 30    # -> cls.age == 30
    cls.age = 's'   # TypeError

これで、型が違ったら書き換えしてはいけないようにできました。

さいごに

Pythonで型を縛る理由がわからんという人はライブラリーを開発してみればそのツラさがわかるので、ライブラリーを開発してみましょう。