限りなくゼロコストで型による値の確からしさを保証するためのテクニックにNewTypeがある。
その紹介。
問題設定
アプリケーションを静的型などを用いてなるべく固めに書きたいときによくやるのは、単なる値に対しても専用の型を用意して値の確からしさを静的にチェックさせる。
class UserId(int):
pass
def get_user_by_id(user_id: UserId) ->...:
...
get_user_by_id(UserId(1)) # ok
get_user_by_id(1) # type error
この様な実装にすればプリミティブとしては一緒の他のint型の値の混入を防ぐことができる。
しかしこれの問題点としてはわざわざ静的にチェックさせるための代償としてruntimeにも実際のオブジェクトが作成されてしまいoverheadが生まれてしまうという事がある。
これを回避するのがNewTypeというテクニックである。
NewTypeの使い方
pythonのtypingモジュールにはNewTypeという関数が用意されていて、それを使用することで静的チェックの恩恵を受けつつruntimeのoverheadを限りなく減らすことができるようになっている。
まずは使い方。
from typing import NewType
UserId = NewType("UserId", int)
def get_user_by_id(user_id: UserId) ->...:
...
get_user_by_id(UserId(1))
get_user_by_id(1) # type error
UserId("id") # type error
この様に実際にクラスを用いて専用型を用意していたときと変わらずに型による恩恵を受けることができる。
あとはどうやって実行時のoverheadをなくしているのか実装をのぞいてみよう。
NewTypeの実装
def NewType(name, tp):
def new_type(x):
return x
new_type.__name__ = name
new_type.__supertype__ = tp
return new_type
NewTypeは関数である。
実際にやっていることはローカルスコープにnew_typeという関数を定義して、色々あるが結局はその宣言した関数をreturnしているだけである。
つまりUserId = NewType(“UserId”, int)の様に宣言した型は実際にはただの関数である。
しかもその関数の挙動はただ入ってきた値をreturnしているだけである。
要約するとNewTypeの挙動は以下のような感じになる。
def new_type(x):
return x
UserId = new_type
これからわかるようにNewTypeで作成した型を使う際には実際にはただの値を使っているに等しい。
UserId(1) = new_type(1) = 1
この様にして実行時のoverheadをなるべく減らしているということだ。
それ以外にやっている__name__や__supertype__の代入はあくまでも型チェックに必要な名前やsupertypeを設定しているだけだと思われる。
このようにしてNewTypeは実行時のoverheadをなるべく減らしつつ静的型による恩恵を受けることを可能にしている。