この記事で行うこと
要素の数が、[ある数値以上、ある数値以下]であるlist配列であること、という型制約を持つ自作クラス(自作データ型)をPythonで宣言してみます。
このデータ型のインスタンスを生成するときに、コンストラクタに、上記の条件を破る引数を与えると、TypeErrorが発生して、Pythonは処理を終了します。
なお、このlist配列は、任意のデータ型のインスタンスを要素に持つことができるという条件も、おまけに付け加えてみます。
さらに、複数の異なるデータ型のインスタンスを要素に持つことも許容します。
上記の条件をすべて満たすデータ型を、Pythonで定義して、使ってみます。
( 関連記事 )
( 必要な外部資源 )
ここでは、以下のGitHubリポジトリにあるPython用のモジュールをgit cloneして、利用します。
・ (GitHubリポジトリ)vixrant/python-type-theory
% git clone https://github.com/vixrant/python-type-theory
Cloning into 'python-type-theory'...
remote: Enumerating objects: 79, done.
remote: Counting objects: 100% (79/79), done.
remote: Compressing objects: 100% (61/61), done.
remote: Total 79 (delta 36), reused 50 (delta 18), pack-reused 0
Unpacking objects: 100% (79/79), done.
%
( 実行環境 )
・ IPython対話型インタプリタ(Python 3.9.0)
git cloneしたPythonスクリプトは、内部でinspect.getsource()メソッドを呼び出すのですが、Pythonの対話型インタプリタでこのメソッドを使うと、inspect.py, raise OSError('could not get source code')エラーが起きるためです。
iPythonの対話型インタプリタは、このエラーを発生させずに、inspect.getsource()メソッドを使うことができるようです。
なお、iPythonを使わずに、Python2系やPython3系を使う場合でも、Pythonのスクリプトファイルを実行する場合は、OSエラーを発生させずに、今回 git cloneしたモジュールを使うことができます。
では、はじめます。
( 利用するモジュールのインポート )
from typing import List, Sequence, TypeVar
from refinement import refine, reftype
T = TypeVar('T')
まず、ある条件を記述し、条件を満たしたかどうかを、boolean(bool)値で返すメソッドを定義します。
今回は、MinN( )メソッドと、MaxN( )メソッドという2つのメソッドを定義しました。
このメソッドには、@reftypeデコーダが付されています。
@reftypeデコーダを付けることで、データ型(自作クラス)が定義されます。
@reftype
def MinN(i: int) -> bool:
return i > 0
@reftype
def MaxN(i: int) -> bool:
return i > 0
出来上がるデータ型(自作クラス)は、上記のメソッドに記述した条件式を満たす値だけを、持つことができる型になります。
MinN型とMaxN型という2つのデータ型(自作クラス)は、以下の条件を満たすインスタンスのみを生成することができます。
・ 条件1. int型の値を持つこと。
・ 条件2. 0より大きい値を持つこと。
上記の2つの条件を充足することを義務付けられたデータ型(クラス)は、依存型(Dependent Type)と呼ばれています。
依存型(Dependent Type)クラスのインスタンスは、ある定められた条件を満たすデータ値だけを、インスタンスとして持つことを許されます。
・ Dependent Types と Refinement Types の違い
・ 型をさらに拡張するーーRefinement Typesについて
先程も述べたように、例えばPositive-Integerであるならば、負の整数に関しては排除されるべきだろう。そのように考えるとするならば、次のような表現だってできるはずだ(以下の例は論文によるものである)
(: max : [x : Int ] [y : Int] ~>
(Refine [z : Int ]
(and (>= z x) (>= z y))))
(define (max x y) (if (> x y) x y))
このように、型に属するであろう任意の要素を取ってきて、それに当てはまるかどうかをチェックすることによって、より厳密な型によるチェックが行える。この場合であるならば、「Int型に属するx、及びyは、必ずzというより上位の数を所持している」ということになる(考えてみればわかるように、もし、xやyに、上位のzの数が存在しないと仮定すると、関数定義において、(> x y)という比較は成りたたない)
依存型は、「項(数値など)に依存して決まる型」と、定義されています。
ここでいう「項(数値など)」とは、そのデータ型がとりうる値を厳格に規定する条件値のことです。以下に例を挙げてみます。
【 インスタンスが持つことのできる値の制約条件(型制約)の例 】
1. int型のデータの場合:値の数の範囲(例:「マイナス100〜プラス150」)
2. str型やList[T]型の場合:配列の長さ(要素の数、文字数)の下限値と上限値
依存型という言葉は、Pythonの専門用語ではなく、関数型言語などのプログラミング言語の領域で使われている、プログラミング言語論の専門用語です。
IdrisやRacket(v6.11以降)と呼ばれるプログラミング言語が、これらの型を宣言し、静的な型検査を行うことができる言語です。
次に、新しいメソッドを定義します。
今度は、定義するメソッドに、@refineデコーダを付けたメソッドの中で、さらに、@reftypeデコレータが付いた別のメソッドを定義します。
@refine
def ListLengthChecker(min_length: MinN, max_length: MaxN):
@reftype
def LenLimit(l: list) -> bool:
return (len(l) >= min_length) and (len(l) <= max_length)
return LenLimit
まず、内側にある@reftypeデコレータが付いたメソッドは、データ区間[min_length以上、max_length以下の値]という条件を満たすデータ値のみをとることが許されたデータ型を、定義するものです。
ここで、ListLengthChecker( )メソッドの型アノテーションを見ると、min_lengthとmin_lengthは、それぞれ、MinN型とMaxN型のインスタンスを持つこと、という定義がなされています。
MinN型とMaxN型は、最初に定義された、「0よりも大きな数値(int)を持つことだけが許容」されたデータ型でした。
ListLengthChecker( )メソッドには、@Refineデコーダが付いています。
ListLengthChecker メソッドに、@Refineデコーダを付けることで、新たにListLengthChecker型という新たなデータ型(クラス)が定義されます。
Refinement Typeは、和訳では、「篩(ふるい)型」と呼ばれています。この型は、「型がとりうる値が、述語で修飾されている型」という説明のされ方をします。
Wikipedia(英語)の定義を引用してみます。
In type theory, a refinement type[1][2][3] is a type endowed with a predicate which is assumed to hold for any element of the refined type.
Refinement types can express preconditions when used as function arguments or postconditions when used as return types:
( 使い方 )
以下の2つのステップを踏みます。
【 ステップ1 】
ListLengthChecker型のコンストラクタに、2つの引数(min_length および max_length)を与えて、ListLengthChecker型のインスタンスを生成する。
【 ステップ2 】
「ステップ1」で生成したListLengthCheckerr型のインスタンスに、任意のデータ型(T)を要素にもつ配列(Sequence)を引数に渡す。
コードで書くと、次のようになります。
temp_func = ListLengthChecker(min_length, max_length)
result = temp_func(data_list)
上記のコードを実行すると、以下の条件をすべて満たすかどうかが、上記のコードを実行する際に、自動的に型制約チェックが走ります。
・ 条件1. 引数として受け取ったMinN, MaxNは、int型の値を持つか。
・ 条件2. 引数として受け取ったMinN, MaxNは、0より大きい値を持つか。
・ 条件3. 引数として受け取ったSequence[T]型のインスタンス data_listの要素の数は、データ区間[min_length以上、max_length以下の値]の数値範囲の中にある値であるか。
これら3つの型制約(条件)のうち、どれか1つでも満たされない制約があると、上記のコードは、「型制約エラー TypeError」を吐いて、処理が止まります。
mypyは、Pythonのスクリプトファイルを外側から「型制約チェック」を行いますが、ここでは、Pythonスクリプトまたは、Pythonの対話型インタプリタで上記のコードが実行される瞬間に、コード内部で、上記の「型制約」が充足されているかどうかのチェックが走ります。
それでは、以下のコードを1つのメソッドに包んでみます。
temp_func = ListLengthChecker(min_length, max_length)
result = temp_func(data_list)
メソッド名として、list_length_checkerという名前を付けてみました。
def LengthRestrictedList(data_list : Sequence[T], min_length : int, max_length : int) -> Sequence[T]:
temp_func = ListLengthChecker(min_length, max_length)
try:
result = temp_func(data_list)
except TypeError:
error_message = "配列リストの長さが条件を満たしません。"
print(error_message)
result = []
return result
なお、引数として受け取るPythonのlist(配列)data_listの型アノテーションは、typingモジュールのSequence[T]でも、List[T]でも、どちらでも構いません。
今回は、data_listの中身の要素は、任意のデータ型を持てるよう許容してみたいと思います。そのため、typingモジュールのジェネリクス(総称型)の「型変数」であるT を指定しています。
data_listの中身の要素を、intやstrやcallableなどの特定のデータ型のインスタンスに限定したい場合は、List[int]やSequence[int]などと、定義します。
def LengthRestrictedList2(data_list : List[T], min_length : int, max_length : int) -> List[T]:
temp_func = ListLengthChecker(min_length, max_length)
try:
result = temp_func(data_list)
except TypeError:
error_message = "配列リストの長さが条件を満たしません。"
print(error_message)
result = []
return result
( 挙動を確認 )
以下で見るように、すべてが、意図した通りの挙動を示しました。(成功!)
result = LengthRestrictedList(["a", "b", "c", "e", "f"], 2, 7)
print(result)
# 実行結果
['a', 'b', 'c', 'e', 'f']
result = LengthRestrictedList(["a", 1, "c", "e", "f"], 2, 7)
print(result)
# 実行結果
['a', 1, 'c', 'e', 'f']
result = LengthRestrictedList(["a"], 2, 7)
# 実行結果
配列リストの長さが条件を満たしません。
result = LengthRestrictedList(["a", 1, "c", "e", "f"], 7, 10)
# 実行結果
配列リストの長さが条件を満たしません。
result = LengthRestrictedList2(["a", "b", "c", "e", "f"], 2, 7)
print(result)
# 実行結果
['a', 'b', 'c', 'e', 'f']
result = LengthRestrictedList2(["a", 1, "c", "e", "f"], 2, 7)
print(result)
# 実行結果
['a', 1, 'c', 'e', 'f']
del result
# 実行結果
result = LengthRestrictedList2(["a", "b", "c", "e", "f"], 2, 7)
print(result)
# 実行結果
['a', 'b', 'c', 'e', 'f']
result = LengthRestrictedList2(["a", 1, "c", "e", "f"], 2, 7)
# 実行結果
print(result)
['a', 1, 'c', 'e', 'f']
result = LengthRestrictedList2(["a"], 2, 7)
# 実行結果
配列リストの長さが条件を満たしません。
result = LengthRestrictedList2(["a", 1, "c", "e", "f"], 7, 10)
# 実行結果
配列リストの長さが条件を満たしません。