daniele-levis-pelusi-OgZb4X8-IdA-unsplash.jpg

PythonとNewType

 
0
このエントリーをはてなブックマークに追加
Kazuki Moriyama
Kazuki Moriyama (森山 和樹)

はじめに

この記事では、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 は実行時のオーバーヘッドをなるべく減らしつつ、静的型による恩恵を受けることを可能にしています。

info-outline

お知らせ

K.DEVは株式会社KDOTにより運営されています。記事の内容や会社でのITに関わる一般的なご相談に専門の社員がお答えしております。ぜひお気軽にご連絡ください。