0. はじめに
Python3系の型チェックについては、typingとmypyが知られています。
・@mski_iksmさんのQiita記事「typehintとmypyを使ったpythonの型チェック」
・@papi_tokeiさんのQiita記事「実践!!Python型入門(Type Hints)」
・SWEet 「Pythonで型検査しようぜ」
・mizzsugar’s blog 「PythonとTypeScriptで学ぶGenerics初めの一歩」
このmypyは、Pythonのスクリプトファイルの外から、型アノテーション制約が守られているかどうかを、チェックするものです。
assert文と列挙型Enumを使うと、メソッドの引数や返り値の型と値のチェックを、スクリプトファイルの内部で閉じた形(自己完結的)に、行うことができます。
同じことを、デコレータを使って実装することも可能です。
1. assertとEnumを使う方法
( 参考 )
・note.nkmk.me 「Pythonで型を取得・判定するtype関数, isinstance関数」
・CodeZine 「Pythonで本当に役立つ機能「アサーション」の使い方を解説!『Pythonトリック』から」
・assert 文ってなに?
( 問題設定 )
- spacyを使って、文章から特定の「固有表現」単語を抽出する。指定された「固有表現」に属する単語が複数見つかった場合、それぞれの単語が、文章中に何回登場したのかをカウントし、出現頻度が多い順番に、単語を返す。
- spacyが認識できる「固有表現」の種類(ラベル)には限りがある。
- ユーザは、どの「固有表現」の単語を文章から抜き出したいのかを、引数として渡す。
- メソッドは、2つの引数を受け取る。1つ目は、解析対象の文章(str型である必要がある)であり、2つ目は、「固有表現」のラベル(名)である。
- 引数として渡された文章データが、str型でないとき、エラーを返す。また、引数として渡された「固有表現」ラベル名が、定義済みの「固有表現」ラベルの中に見つからない場合は、エラーを返す。
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, '入力された固有表現ラベルはまだ定義されていません。'
# 受け取った2つの引数の型と値に問題がなければ、以下を実行
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)
text = """今日僕はメアリーとアメリカにやってきた。フランスのパリを経由してきたんだ。"""
NamedEntityLabel.extract_named_entity_wordlist(text, "Chimei")
# 結果
['アメリカ', 'フランス', 'パリ']
NamedEntityLabel.extract_named_entity_wordlist(text, "Jinmei")
# 結果
['メアリー']
( 引数に、意図しない値が渡された場合 )
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: 入力された固有表現ラベルはまだ定義されていません
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でないといけません。
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の値から文字列を取得する、一覧を取得する、振る舞いを持たせる」
・Python学習チャンネル by PyQ「Pythonのクラスメソッド(@classmethod)とは?使いどころとメソッドとの違いを解説」
次に、デコレータを使った方法を見てみます。
2. assertを使わずに、デコレータとEnumを使う方法
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))
( 使い方 )
NamedEntityLabel.value_check("Jinmei")
# 実行結果
NamedEntityLabel.value_check("Jinmei")
<NamedEntityLabel.Jinmei: 'Person'>
NamedEntityLabel.value_check("EmailAddress")
# 実行結果
ValueError: EmailAddress は、定義された固有表現ラベルではありません
上記を利用して、以下のコードを作成
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)
# 受け取った2つの引数の型と値に問題がなければ、以下を実行
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
( 引数に、意図された値が渡された場合 )
text = """今日僕はメアリーとアメリカにやってきた。フランスのパリを経由してきたんだ。"""
NamedEntityLabel_2.extract_named_entity_wordlist(text, "Chimei")
# 結果
['アメリカ', 'フランス', 'パリ']
( 引数に、意図しない値が渡された場合 )
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 は、定義された固有表現ラベルではありません
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で関数の引数型チェックをデコレータで実装する」
3. 受け取った引数をコンストラクタに渡して生成したEnumクラスのインスタンスを、本体の処理で使う方法
以下が一番、簡潔です。
( 方法 )
- 受けとった固有表現ラベル名を、(固有表現が定義された)列挙型クラスのインスタンス・コンストラクタに渡して、固有表現クラスのインスタンスを生成する。
- データ処理は、生成した固有表現クラスのインスタンスを使う。
- 未定義の固有表現ラベルを受け取った場合は、固有表現クラスのインスタンスを生成する段階で、Keyエラーが発生し、以後の処理は行われない。
まず、適当な固有表現ラベル名をコンストラクタに渡して、(固有表現が定義された)列挙型クラスのインスタンスを生成しようとすると、挙動がどうなるのかを確認。
列挙型(Enum)クラスの宣言
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の場合 )
インスタンスは無事に生成される
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の場合 )
インスタンスは生成されず、エラーが出る
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の場合 )
インスタンスは無事に生成される
# 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の場合 )
インスタンスは生成されず、エラーが出る
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」
(1) Enumクラスを定義
固有表現ラベル名として、利用可能なものを列挙型クラスとして定義
class NamedEntityLabel_3(Enum):
Jinmei : str = "PERSON"
Chimei : str = "LOC"
(2) 本体部分の処理を行うメソッドを定義
以下の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]
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]
# 受け取った2つの引数の型と値に問題がなければ、以下を実行
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
( 引数に、意図された値が渡された場合 )
extract_named_entity_wordlist(text, "Chimei")
# 実行結果
['アメリカ', 'フランス', 'パリ']
( 引数に、意図しない値が渡された場合 )
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)で定義しました。