Help us understand the problem. What is going on with this article?

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

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. assertEnumを使う方法

( 参考 )

note.nkmk.me 「Pythonで型を取得・判定するtype関数, isinstance関数」
CodeZine 「Pythonで本当に役立つ機能「アサーション」の使い方を解説!『Pythonトリック』から」
assert 文ってなに?

( 問題設定 )

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

Python3
text = """今日僕はメアリーとアメリカにやってきた。フランスのパリを経由してきたんだ。"""

NamedEntityLabel.extract_named_entity_wordlist(text, "Chimei")
# 結果
['アメリカ', 'フランス', 'パリ']

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

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

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
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
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を使う方法

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
NamedEntityLabel.value_check("Jinmei")

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

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

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

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)
        # 受け取った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

( 引数に、意図された値が渡された場合 )

Python3
text = """今日僕はメアリーとアメリカにやってきた。フランスのパリを経由してきたんだ。"""

NamedEntityLabel_2.extract_named_entity_wordlist(text, "Chimei")
# 結果
['アメリカ', 'フランス', 'パリ']

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

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
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クラスのインスタンスを、本体の処理で使う方法

以下が一番、簡潔です。

( 方法 )

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

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

列挙型(Enum)クラスの宣言

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
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
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
#  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
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クラスを定義

固有表現ラベル名として、利用可能なものを列挙型クラスとして定義
Python3
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]

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]
    # 受け取った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

( 引数に、意図された値が渡された場合 )

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

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

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型)オブジェクトの中で、keyvalueのペアとして定義しても良い。
・ しかし、ここでは、「定義済みの固有表現の種類」を、コード上で明示的に記述するために、「固有表現クラス」というクラスを列挙型(Enum)で定義しました。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away