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で型を縛る理由がわからんという人はライブラリーを開発してみればそのツラさがわかるので、ライブラリーを開発してみましょう。
- 前の記事
巴戦を状態遷移図で解いてみよう 2021.05.05
- 次の記事
SeleniumのChromeでexecutable_pathを毎回指定するのが面倒だから改善した 2021.09.11