Pythonでインスタンス変数の入力値をバリデーションする方法

2020.02.18

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

今回はPythonのクラスでインスタンス変数の入力時にバリデーションチェックを行う方法を調べました。

ここでの入力値のバリデーションとは、

  • コンストラクタ__init__内でのバリデーション
  • setterメソッド内でのバリデーション

を指します。

要は、どちらの入力時にも同様のバリデーションチェックが走るようなコードの書きたかったのですが、いくつかやり方があったのでまとめてみました。

方法1. 愚直に書く

  • コンストラクタ__init__でバリデーションを書く
  • セッターメソッド@age.setterを持っているインスタンス変数(ここでは、age)は、セッターメソッド内でバリデーションを書いて処理を共通化

コード

class User:

    def __init__(self, name: str, age: int):
        # コンストラクタ内でバリデーション処理を実装
        if type(name) is not str:
            raise TypeError('name must be str')
        self.__name = name
        # セッターメソッド側でバリデーション処理を共通化
        self.age = age

    @property
    def name(self):
        return self.__name

    @property
    def age(self):
        return self.__age

    @age.setter
    def age(self, val: int):
        if type(val) is not int:
            raise TypeError('age must be int')
        self.__age = val

動作確認

# 初期化
# 不正な値を入れてみる
user = User(name=False, age=False)
>>> TypeError: name must be str
# 正しい値を入れてみる
user = User(name='arai', age=31)


# セッター呼び出し
# nameの値を書き換えてみる
user.name = 'seiichi'
>>> AttributeError: can't set attribute
# ageの値を書き換えてみる
user.age = 17
>>> TypeError: int must be int
user.age = '31'

所感

これでも問題はないのですが、コンストラクトとセッターでバリデーションチェックを行っており全体的なコードの見通しが悪いのかなと感じました。

方法2. attrsライブラリをつかう

attrs ライブラリに自動バリデーション機能をデコレータで追加してみた」 を参考にさせて頂きました。

attrsを利用することで、より簡潔に書くことができます。

ソースコード

import attr

@attr.s
class User:
    name = attr.ib(validator=attr.validators.instance_of(str))
    age = attr.ib(validator=attr.validators.instance_of(int))

動作確認

# 初期化
# 不正な値を入れてみる
user = User(name=False, age=False)
# >>> TypeError: ("'name' must be <class 'str'> (got False that is a <class 'bool'>).", Attribute(name='name', default=NOTHING, validator=<instance_of validator for type <class 'str'>>, repr=True, eq=True, order=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False), <class 'str'>, False)
# 正しい値を入れてみる
user = User(name='arai', age=31)


# セッター呼び出し
# nameの値を書き換えてみる
user.name = 'seiichi'
# ageの値を書き換えてみる
user.age = 17
user.age = '31'

所感

参考にしたプログの方でも指摘されていますが、 attrsでは後からインスタンス変数が変更された場合(setter)では、バリデーションチェックが走らないという大きな問題があります。2年ほど前からissueに挙がっていますが、まだクローズされていません。

その他個人的に気になったのは、勝手にsetterメソッドが定義される点です。できれば、user.name = 'seiichi'はエラーになってほしいです。

一応@attr.s(frozen=True)をつけてオブジェクト自体をイミュータブルにする方法もありますが、やりたいこととは違います。

そもそもPythonではsetterの有無にかかわらず変数や関数は参照可能なので、仮にsetterを未定義にしても変数の変更を防げる保証はないため、あまりこだわりすぎる必要はないのですが。

方法3. pyfiledライブラリをつかう

先ほどのissueの質問者が作成したpyfiledsというライブラリを試してみました。

pyfieldsでは、型チェック、独自バリデーションなどを簡潔に書くことができます。

ソースコード

from pyfields import field, make_init

class User(object):
    name:str = field(check_type=True, read_only=True, doc='name must be str')
    age:int = field(check_type=True, doc='age must be int')
    __init__ = make_init()

動作確認

# 初期化
# 不正な値を入れてみる
user = User(name=False, age=False)
>>> FieldTypeError: Invalid value type provided for 'User.name'. Value should be of type <class 'str'>. Instead, received a 'bool': False
# 正しい値を入れてみる
user = User(name='arai', age=31)


# セッター呼び出し
# nameの値を書き換えてみる
user.name = 'seiichi'
>>> ReadOnlyFieldError: Read-only field 'User.name' has already been initialized on instance <__main__.User object at 0x> and cannot be modified anymore.
# ageの値を書き換えてみる
user.age = 17
user.age = '31'
>>> FieldTypeError: Invalid value type provided for 'User.age'. Value should be of type <class 'int'>. Instead, received a 'str': '31'

所感

リリースされて間もないですが、ドキュメントも結構しっかりしており、個人的には一番使いやすかったです。

更新頻度は結構高く、2019/11/17には安定版がでています。

いろいろできることも多いので、機会があったらここらへんもまとめてブログ化したいと思います。

まとめ

いかがだったでしょうか。

他にいい方法を知っているという方いれば是非おしえてください。