PythonとNewType
はじめに
この記事では、Pythonにおける 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) # 型エラー
このような実装により、プリミティブとしては一緒の他の int
型の値の混入を防ぐことができます。ただし、この方法の問題点は、値の確かさを静的にチェックさせるための代償として、ランタイムに実際のオブジェクトが作成されるため、オーバーヘッドが発生することです。
NewTypeの使い方
Pythonのtypingモジュールには NewType
という関数が用意されており、これを使用することで静的チェックの恩恵を受けつつ、ランタイムのオーバーヘッドを限りなく減らすことができます。
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) # 型エラー
UserId("id") # 型エラー
このように、実際にクラスを用いて専用の型を用意していた場合と同様に、型による恩恵を受けることができます。また、実行時のオーバーヘッドをなくしている方法についても見ていきましょう。
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
しているだけです。
具体的には以下のようになります。
def new_type(x):
return x
UserId = new_type
これにより、 UserId(1)
は実際にはただの値を使っているのと同じです。
UserId(1) = new_type(1) = 1
このようにして、実行時のオーバーヘッドをなるべく減らしているということです。NewType
が行っている __name__
や __supertype__
の代入は、あくまでも型チェックに必要な名前やスーパータイプを設定しているだけです。
このようにして、 NewType
は実行時のオーバーヘッドをなるべく減らしつつ、静的型による恩恵を受けることを可能にしています。