attrs-validators

バリデーションPythonattrsデータクラス型検証属性検証

バリデーションライブラリ

attrs-validators

概要

attrs-validatorsは、Pythonのattrsライブラリに統合されたバリデーション機能です。データクラスの定義と同時にバリデーションロジックを埋め込むことができ、型安全で堅牢なPythonコードの作成を支援します。attrsのデータクラスに対して、組み込みバリデータやカスタムバリデータを使用して、実行時の値検証を提供します。

詳細

attrs-validatorsはattrsライブラリの一部として提供される強力なバリデーション機能です。attrsは、Pythonのデータクラスを作成するための先進的なライブラリで、標準のdataclassesよりも多くの機能を提供します。バリデーション機能はattr.validatorsモジュールを通じて利用でき、型チェック、値の範囲検証、カスタムバリデーションロジックなど、さまざまな検証パターンをサポートしています。

主な特徴として、宣言的なバリデーション定義、コンバータとの組み合わせ、複数バリデータのチェイン、属性間の相互検証などがあります。また、バリデーションは初期化時だけでなく、属性の設定時にも実行できるため、オブジェクトの整合性を常に保証できます。

メリット・デメリット

メリット

  • 宣言的な構文: クラス定義と同時にバリデーションルールを明確に定義できる
  • 豊富な組み込みバリデータ: instance_of、in_、ge/gt/le/lt、deep_iterableなど、よく使うバリデータが組み込まれている
  • カスタムバリデータの簡単な作成: デコレータを使用して直感的にカスタムバリデータを定義可能
  • コンバータとの統合: 値の変換後にバリデーションを実行できるため、柔軟な入力処理が可能
  • 属性間の相互検証: 複数の属性値を参照する複雑なバリデーションロジックも実装可能
  • パフォーマンス: C拡張による高速な実行と、バリデーションの無効化オプション

デメリット

  • attrsライブラリへの依存: attrsライブラリ全体を使用する必要があり、既存のプロジェクトへの導入が難しい場合がある
  • 学習曲線: attrsの独自構文に慣れる必要がある
  • 標準dataclassesとの非互換: Pythonの標準dataclassesとは異なるアプローチのため、移行が困難
  • デコレータの多用: 複雑なバリデーションでは多くのデコレータが必要になり、コードが読みにくくなる可能性

参考ページ

書き方の例

基本的な使い方

import attr
from attr import validators

@attr.s
class User:
    # 型検証
    name = attr.ib(validator=validators.instance_of(str))
    
    # 範囲検証
    age = attr.ib(validator=[
        validators.instance_of(int),
        validators.ge(0),  # 0以上
        validators.le(150)  # 150以下
    ])
    
    # 選択肢検証
    status = attr.ib(validator=validators.in_(["active", "inactive"]))

# 使用例
user = User(name="田中", age=25, status="active")
# user = User(name="田中", age=-1, status="active")  # ValueError

カスタムバリデータ

from attrs import define, field

@define
class Product:
    name: str = field()
    price: float = field()
    quantity: int = field()
    
    @price.validator
    def validate_price(self, attribute, value):
        if value <= 0:
            raise ValueError("価格は0より大きい必要があります")
        if value > 1000000:
            raise ValueError("価格は100万円以下にしてください")
    
    @quantity.validator
    def validate_quantity(self, attribute, value):
        if value < 0:
            raise ValueError("数量は負の値にできません")

# 使用例
product = Product(name="ノートPC", price=89800, quantity=10)

コンバータとバリデータの組み合わせ

import attr
from attr import validators

def to_upper(value):
    """文字列を大文字に変換"""
    return value.upper() if isinstance(value, str) else value

@attr.s
class Code:
    # 変換してからバリデーション
    country_code = attr.ib(
        converter=to_upper,
        validator=[
            validators.instance_of(str),
            validators.in_(["JP", "US", "UK", "CN"])
        ]
    )
    
    postal_code = attr.ib(
        converter=str,  # 数値も文字列に変換
        validator=validators.matches_re(r"^\d{3}-\d{4}$")
    )

# 使用例
code = Code(country_code="jp", postal_code="123-4567")  # "jp"は"JP"に変換される
print(code.country_code)  # "JP"

属性間の相互検証

from attrs import define, field

def end_after_start(instance, attribute, value):
    """終了日が開始日より後であることを検証"""
    if instance.start_date and value <= instance.start_date:
        raise ValueError("終了日は開始日より後である必要があります")

@define
class DateRange:
    start_date: str = field()
    end_date: str = field(validator=end_after_start)
    
    @start_date.validator
    def validate_start_date(self, attribute, value):
        # 日付形式の検証
        try:
            from datetime import datetime
            datetime.strptime(value, "%Y-%m-%d")
        except ValueError:
            raise ValueError("日付はYYYY-MM-DD形式で入力してください")

# 使用例
date_range = DateRange(start_date="2024-01-01", end_date="2024-12-31")

論理演算子を使った複合バリデーション

from attrs import define, field, validators

@define
class FlexibleInput:
    # 文字列または整数を受け入れる
    value = field(validator=validators.or_(
        validators.instance_of(str),
        validators.instance_of(int)
    ))
    
    # オプショナルなメールアドレス
    email = field(
        default=None,
        validator=validators.optional(
            validators.matches_re(r"^[\w\.-]+@[\w\.-]+\.\w+$")
        )
    )

# 使用例
input1 = FlexibleInput(value="text")
input2 = FlexibleInput(value=123, email="[email protected]")
input3 = FlexibleInput(value=123)  # emailはNone(オプショナル)

バリデーションの動的な制御

import attr
from attr import validators

# バリデーションを一時的に無効化
with validators.disabled():
    @attr.s
    class TempData:
        value = attr.ib(validator=validators.instance_of(int))
    
    # バリデーションが無効なので、文字列でもエラーにならない
    temp = TempData(value="not an int")

# コンテキスト外ではバリデーションが有効
# temp2 = TempData(value="not an int")  # これはエラーになる