assertとEnum(or)デコレータを使うと、型アノテーション制約の遵守チェックをmypyの手を借りずに行える

##０. はじめに
Python3系の型チェックについては、typingとmypyが知られています。

___
・[@mski_iksmさんのQiita記事「typehintとmypyを使ったpythonの型チェック」](https://qiita.com/mski_iksm/items/85651837bd70427754d6)
・[@papi_tokeiさんのQiita記事「実践！！Python型入門(Type Hints)」](https://qiita.com/papi_tokei/items/2a309d313bc6fc5661c3)
・[SWEet 「Pythonで型検査しようぜ」](https://kk-river108.hatenablog.com/entry/2019/03/10/163457)
・[mizzsugar’s blog 「PythonとTypeScriptで学ぶGenerics初めの一歩」](https://mizzsugar.hatenablog.com/entry/2020/02/07/002250)
___

__この*mypy*は、Pythonのスクリプトファイルの外から、型アノテーション制約が守られているかどうかを、チェックするものです。__

__*assert*文と列挙型*Enum*を使うと、メソッドの引数や返り値の型と値のチェックを、スクリプトファイルの内部で閉じた形（自己完結的）に、行うことができます。__

__同じことを、デコレータを使って実装することも可能です。__

##１. *assert*と*Enum*を使う方法

####（ 参考 ）

・[note.nkmk.me 「Pythonで型を取得・判定するtype関数, isinstance関数」](https://note.nkmk.me/python-type-isinstance/)
・[CodeZine 「Pythonで本当に役立つ機能「アサーション」の使い方を解説！『Pythonトリック』から」 ](https://codezine.jp/article/detail/12179)
・[assert 文ってなに？](https://python.ms/assert/#_1-%E3%81%84%E3%81%A4%E4%BD%BF%E3%81%86%E3%81%AE)

####（ 問題設定 ）

1. *spacy*を使って、文章から特定の「固有表現」単語を抽出する。指定された「固有表現」に属する単語が複数見つかった場合、それぞれの単語が、文章中に何回登場したのかをカウントし、出現頻度が多い順番に、単語を返す。
2. *spacy*が認識できる「固有表現」の種類（ラベル）には限りがある。
3. ユーザは、どの「固有表現」の単語を文章から抜き出したいのかを、引数として渡す。
4. メソッドは、２つの引数を受け取る。１つ目は、解析対象の文章（*str*型である必要がある）であり、２つ目は、「固有表現」のラベル（名）である。
5. 引数として渡された文章データが、*str*型でないとき、エラーを返す。また、引数として渡された「固有表現」ラベル名が、定義済みの「固有表現」ラベルの中に見つからない場合は、エラーを返す。

```Python3:Python3
from enum import Enum
from typing import List, Dict
import spacy

class NamedEntityLabel(Enum):
	Jinmei : str = "PERSON"
	Chimei : str = "LOC"

	def extract_named_entity_wordlist(text : str, ne_label : str) -> List[str]:
		# 第一引数の型をチェック
		assert type(text) is str, '入力するテキストデータはstringでないといけません。'
		# 第二引数の値をチェック
		right_value_list = [e.name for e in NamedEntityLabel]
		assert ne_label in right_value_list, '入力された固有表現ラベルはまだ定義されていません。'
		# 受け取った２つの引数の型と値に問題がなければ、以下を実行
    	nlp = spacy.load('ja_ginza')
   	 	text = text.replace("\n", "")
    	doc = nlp(text)
    	word_list = [ent.text for ent in doc.ents if ent.label_ == NamedEntityLabel[ne_label].value]
    	return word_list
    	
```

####方針
__*if ne_label not in ["PERSON", "LOC"]*と書いても良いが、ここでは「定義済みの固有表現の種類」をコード上で明示的に記述するために、「固有表現クラス」というクラスを、列挙型（*Enum*）で定義する。__

###使ってみる

####（ 引数に、意図された値が渡された場合 ）

__受け取った*text*文から、指示された固有表現ラベルに該当する単語を抜き出して、各単語を出現頻度順に並べて返す。（固有表現抽出：*Named Entity Recognition*）__

```Python3:Python3
text = """今日僕はメアリーとアメリカにやってきた。フランスのパリを経由してきたんだ。"""
 
NamedEntityLabel.extract_named_entity_wordlist(text, "Chimei")
# 結果
['アメリカ', 'フランス', 'パリ']

NamedEntityLabel.extract_named_entity_wordlist(text, "Jinmei")
# 結果
['メアリー']
```

####（ 引数に、意図しない値が渡された場合 ）
 
 ```Python3:Python3
NamedEntityLabel.extract_named_entity_wordlist(text, "Soshikimei")
# 結果
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 7, in extract_named_entity_wordlist
AssertionError: 入力された固有表現ラベルはまだ定義されていません
```

```Python3:Python3
NamedEntityLabel.extract_named_entity_wordlist(127, "Soshikimei")
# 結果
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in extract_named_entity_wordlist
AssertionError: 入力するテキストデータはstringでないといけません。
```

```Python3:Python3
NamedEntityLabel.extract_named_entity_wordlist(127, "Jinmei")
# 結果
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in extract_named_entity_wordlist
AssertionError: 入力するテキストデータはstringでないといけません。
```
 
___
 
####以下のウェブページの様に、デコレータを使うこともできます。
 
 ・ [NANSYSTEM 「#Python3.7でenumを定義し、文字列からEnumを生成する、Enumの値から文字列を取得する、一覧を取得する、振る舞いを持たせる」](https://nansystem.com/python-enum-definition-and-convert-from-string-and-get-list/)
 ・[Python学習チャンネル by PyQ「Pythonのクラスメソッド（@classmethod）とは？使いどころとメソッドとの違いを解説」](https://blog.pyq.jp/entry/Python_kaiketsu_190205)

####次に、デコレータを使った方法を見てみます。
 
##２. *assert*を使わずに、デコレータと*Enum*を使う方法
 
```Python3:Python3
class NamedEntityLabel(Enum):
	Jinmei : str = "Person"
	Chimei : str = "LOC"
	Soshikimei :str = "ORG"
    @classmethod
    def value_check(cls, target_value):
        for e in NamedEntityLabel:
            if e.name == target_value:
                return e
        raise ValueError('{} は、定義された固有表現ラベルではありません'.format(target_value))
```

####（ 使い方 ）

```Python3:Python3
NamedEntityLabel.value_check("Jinmei")

# 実行結果
NamedEntityLabel.value_check("Jinmei")
<NamedEntityLabel.Jinmei: 'Person'>
``` 

```Python3:Python3
NamedEntityLabel.value_check("EmailAddress")

# 実行結果
ValueError: EmailAddress は、定義された固有表現ラベルではありません
``` 

####上記を利用して、以下のコードを作成

```Python3:Python3
class NamedEntityLabel_2(Enum):
	Jinmei : str = "PERSON"
	Chimei : str = "LOC"
    @classmethod
    def value_check(cls, target_value):
        for e in NamedEntityLabel_2:
            if e.name == target_value:
                return e
        raise ValueError('{} は、定義された固有表現ラベルではありません
'.format(target_value))
        
	def extract_named_entity_wordlist(text : str, ne_label : str) -> List[str]:
		# 第一引数の型をチェック
		assert type(text) is str, '入力するテキストデータはstringでないといけません。'
		# 第二引数の値をチェック
		e = NamedEntityLabel_2.value_check(ne_label)
		# 受け取った２つの引数の型と値に問題がなければ、以下を実行
    	nlp = spacy.load('ja_ginza')
   	 	text = text.replace("\n", "")
    	doc = nlp(text)
    	word_list = [ent.text for ent in doc.ents if ent.label_ == e.value]
    	return word_list
 ```
 
####（ 引数に、意図された値が渡された場合 ）

```Python3:Python3
text = """今日僕はメアリーとアメリカにやってきた。フランスのパリを経由してきたんだ。"""
 
NamedEntityLabel_2.extract_named_entity_wordlist(text, "Chimei")
# 結果
['アメリカ', 'フランス', 'パリ']
```

####（ 引数に、意図しない値が渡された場合 ）
 
 ```Python3:Python3
NamedEntityLabel_2.extract_named_entity_wordlist(text, "Soshikimei")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 12, in extract_named_entity_wordlist
  File "<stdin>", line 9, in value_check
ValueError: Soshikimei は、定義された固有表現ラベルではありません
```

```Python3:Python3
NamedEntityLabel_2.extract_named_entity_wordlist(127, "Soshikimei")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 11, in extract_named_entity_wordlist
AssertionError: 入力するテキストデータはstringでないといけません。
```

####（ 参考 ）
メソッドの型アノテーション制約が守られているのか、デコレータを使ってチェックするコードは、以下のWebページでも提案されています。

・[CosmoSonic21 blog 「Pythonで関数の引数型チェックをデコレータで実装する」](https://cosmosonic21.hatenablog.com/entry/2020/01/15/165913)

___

##３. 受け取った引数をコンストラクタに渡して生成した*Enum*クラスのインスタンスを、本体の処理で使う方法
####以下が一番、簡潔です。

####（ 方法 ）
1. 受けとった固有表現ラベル名を、（固有表現が定義された）列挙型クラスのインスタンス・コンストラクタに渡して、固有表現クラスのインスタンスを生成する。
2. データ処理は、生成した固有表現クラスのインスタンスを使う。
3. 未定義の固有表現ラベルを受け取った場合は、固有表現クラスのインスタンスを生成する段階で、Keyエラーが発生し、以後の処理は行われない。

___

#####__まず、適当な固有表現ラベル名をコンストラクタに渡して、（固有表現が定義された）列挙型クラスのインスタンスを生成しようとすると、挙動がどうなるのかを確認。__

__列挙型（*Enum*）クラスの宣言__

```Python3:Python3
from enum import Enum
import enum
from typing import List, Dict

@enum.unique
class NamedEntityLabel(Enum):
Jinmei : str = "PERSON"
Chimei : str = "LOC"
```

__任意のデータをコンストラクタに渡して、列挙型（*Enum*）クラスのインスタンスを生成__


__( 渡した値が、*Enum*クラスで定義済みの*Name*の場合 ）__
__インスタンスは無事に生成される__

```Python3:Python3
named_entity_instance_test = NamedEntityLabel["Jinmei"]
print(named_entity_instance_test)
# 実行結果：インスタンスが無事に生成された
NamedEntityLabel.Jinmei
#  生成したインスタンスの名前を取り出す
print(named_entity_instance_test.name)
# 実行結果
Jinmei
#  生成したインスタンスの値を取り出す
print(named_entity_instance_test.value)
# 実行結果
PERSON
```

__( 渡した値が、*Enum*クラスで未定義の*Name*の場合 ）__
__インスタンスは生成されず、エラーが出る__

```Python3:Python3
named_entity_instance_test = NamedEntityLabel["EMAIL"]
# 実行結果：NamedEntityLabelクラス（列挙型Enum型）で未定義の名前を渡した為、エラーが発生した
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/ocean/.pyenv/versions/3.9.0/lib/python3.9/enum.py", line 355, in __getitem__
    return cls._member_map_[name]
KeyError: 'EMAIL'
```


__( 渡した値が、*Enum*クラスで定義済みの*Name*の場合 ）__
__インスタンスは無事に生成される__

```Python3:Python3
#  NamedEntityLabelクラスのインスタンスを生成する際に、コンストラクタに渡す変数
# この変数は、ユーザから受け取った値を格納したケースを想定
input_data_ok = "Jinmei"
input_data_ng = "Emailaddress"

named_entity_instance_ok = NamedEntityLabel[input_data_ok]
# エラーは起きない
print(named_entity_instance_ok)
# 実行結果：インスタンスが無事に生成されている
NamedEntityLabel.Jinmei
#  生成したインスタンスの名前を取り出す
print(named_entity_instance_ok.name)
# 実行結果
Jinmei
 
#  生成したインスタンスの値を取り出す
print(named_entity_instance_ok.value)
# 実行結果
PERSON
```

__( 渡した値が、*Enum*クラスで未定義の*Name*の場合 ）__
__インスタンスは生成されず、エラーが出る__

```Python3:Python3
named_entity_instance_ng = NamedEntityLabel[input_data_ng]
# 実行結果：NamedEntityLabelクラス（列挙型Enum型）で未定義の名前を渡した為、エラーが発生した
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/ocean/.pyenv/versions/3.9.0/lib/python3.9/enum.py", line 355, in __getitem__
    return cls._member_map_[name]
KeyError: 'Emailaddress'
```

##__以上を受けて、スクリプト全体を書き換える__

・[@ksato9700さんのQiita記事「Python 3.4.0 の新機能 (2) - enum」](https://qiita.com/ksato9700/items/cbad76b9519d1bf05bee)

###(1) *Enum*クラスを定義
#####__固有表現ラベル名として、利用可能なものを列挙型クラスとして定義__

```Python3:Python3
class NamedEntityLabel_3(Enum):
    Jinmei : str = "PERSON"
    Chimei : str = "LOC"

```

###(2) 本体部分の処理を行うメソッドを定義
#####__以下の２行に注目__

・ NamedEntityLabelクラスのインスタンスを利用している

__（1箇所目）__
 *named_entity_instance = NamedEntityLabel_3[ne_label]*

__（2箇所目）__
 *word_list = [ent.text for ent in doc.ents if ent.label_ == named_entity_instance.value]*

```Python3:Python3
def extract_named_entity_wordlist(text : str, ne_label : str) -> List[str]:
	# 第一引数の型をチェック
    assert type(text) is str, '入力するテキストデータはstringでないといけません。'
    # 第二引数の値をチェック
    # 引数で受け取った語が、NamedEntityLabelクラスのNameとして、未定義の語であった場合、この語をコンストラクタに渡してNamedEntityLabelのインスアンスを生成しようとすると、Keyエラーが発生する
    named_entity_instance = NamedEntityLabel_3[ne_label]
    # 受け取った２つの引数の型と値に問題がなければ、以下を実行
    nlp = spacy.load('ja_ginza')
    text = text.replace("\n", "")
    doc = nlp(text)
    word_list = [ent.text for ent in doc.ents if ent.label_ == named_entity_instance.value]
    return word_list

```

（ 引数に、意図された値が渡された場合 ）

```Pythin3:Python3
extract_named_entity_wordlist(text, "Chimei")
# 実行結果
['アメリカ', 'フランス', 'パリ']

```

（ 引数に、意図しない値が渡された場合 ）

```Pythin3:Python3
extract_named_entity_wordlist(text, "Soshikimei")
# 実行結果
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in extract_named_entity_wordlist
  File "/Users/ocean/.pyenv/versions/3.9.0/lib/python3.9/enum.py", line 355, in __getitem__
    return cls._member_map_[name]
KeyError: 'Soshikimei'
```

・ __固有表現ラベルの名前（Name）と値（Value）のペアを、辞書型（*dict*型）オブジェクトの中で、*key*と*value*のペアとして定義しても良い。
・ しかし、ここでは、「定義済みの固有表現の種類」を、コード上で明示的に記述するために、「固有表現クラス」というクラスを列挙型（*Enum*）で定義しました。__