18
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Pythonのclassで値をvalidationする方法4選

Last updated at Posted at 2022-08-24

はじめに

良いコード/悪いコードで学ぶ設計入門」を読んでから、値のバリデーションについて意識するようになりました。Pythonで早速クラスのバリデーションについて調べてみると色々と方法があったので、それぞれ動かしてみました。

バリデーションを行う方法

方法1. クラス内にバリデーション用の関数を作る

class Gadget:
    def __init__(self, name, price, brand):
        self.name = self._validate_name(name)
        self.price = self._validate_price(price)
        self.brand = brand

    def _validate_name(self, name):
        if len(name) > 15:
            raise ValueError("name cannot exceed 15 characters.")
        return name

    def _validate_price(self, price):
        if price < 0:
            raise ValueError("Price cannot be negative.")
        return price


if __name__ == "__main__":
    MBP15 = Gadget(
        name="MacBookPro15",
        price=150000,
        brand="Apple"
    )
    TP = Gadget(
        name="ThinkPadX1Carbon",
        price=120000,
        brand="lenovo"
    )
    # ValueError: name cannot exceed 15 characters.
    foo = Gadget(
        name="Something",
        price=-111,
        brand=None
    )
    # ValueError: Price cannot be negative.

方法1はクラス内にバリデーション用の関数を作る方法です。
この方法はシンプルに見えますが、問題があります。mainの中を以下のように書き換え、実行してみてください。

if __name__ == "__main__":
    MBP15 = Gadget(
        name="MacBookPro15",
        price=150000,
        brand="Apple"
    )
    MBP15.name = "GadgetNameLengthIsMoreThan15."
    print(MBP15.name)

本来であればnameには15文字以上入れたくないのですが、上記は問題なく実行できてしまいます。
さらに__init__関数の中がシンプルなことを好む人が多いのでこの方法は基本なしと感じました。

また、@dataclassを使ったバリデーション方法もありますが、上記のように値を更新するときにバリデーションが行われないそうです。

方法2. @propertyを使う

class Gadget:
    def __init__(self, name, price, brand):
        self.name = name
        self.price = price
        self.brand = brand

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

    @name.setter
    def name(self, value):
        if len(value) > 15:
            raise ValueError("name cannot exceed 15 characters.")
        self._name = value

    @property
    def price(self):
        return self._price

    @price.setter
    def price(self, value):
        if value < 0:
            raise ValueError("Price cannot be negative.")
        self._price = value


if __name__ == "__main__":
    MBP15 = Gadget(
        name="MacBookPro15",
        price=150000,
        brand="Apple"
    )
    TP = Gadget(
        name="ThinkPadX1Carbon",
        price=120000,
        brand="lenovo"
    )
    # ValueError: name cannot exceed 15 characters.
    foo = Gadget(
        name="Something",
        price=-111,
        brand=None
    )
    # ValueError: Price cannot be negative.

Pythonの組み込み関数の@propertyを使った方法は、方法1よりも多くのコードがありますが、__init__関数をシンプルにすることが可能です。さらに方法1であった値更新時にエラーが出ない問題もクリアされています。

if __name__ == "__main__":
    MBP15 = Gadget(
        name="MacBookPro15",
        price=150000,
        brand="Apple"
    )
    MBP15.name = "GadgetNameLengthIsMoreThan15."
    print(MBP15.name)
     # ValueError: name cannot exceed 15 characters.

方法3. Descriptorsを使う

class Name:
    def __get__(self, obj, objtype=None):
        return self.value

    def __set__(self, obj, value):
        if len(value) > 15:
            raise ValueError("name cannot exceed 15 characters.")
        self.value = value


class Price:
    def __get__(self, obj, objtype=None):
        return self.value

    def __set__(self, obj, value):
        if value < 0:
            raise ValueError("Price cannot be negative.")
        self.value = value


class Gadget:

    name = Name()
    price = Price()

    def __init__(self, name, price, brand):
        self.name = name
        self.price = price
        self.brand = brand


if __name__ == "__main__":
    MBP15 = Gadget(
        name="MacBookPro15",
        price=150000,
        brand="Apple"
    )
    TP = Gadget(
        name="ThinkPadX1Carbon",
        price=120000,
        brand="lenovo"
    )
    # ValueError: name cannot exceed 15 characters.
    foo = Gadget(
        name="Something",
        price=-111,
        brand=None
    )
    # ValueError: Price cannot be negative.

3つ目の方法は、__get____set____delete__のメソッド定義する方法です。
この方法で嬉しいことは、例として、Gadgetクラス以外にFurnitureクラスを作る時に、バリデーションの記述を再利用できるとことです。

class Furniture:
  
    name = Name()
    price = Price()

    def __init__(self, name, price, brand):
        self.name = name
        self.price = price
        self.brand = brand

プロジェクトが巨大になった時にはこの方法が一番重宝しそうだと思いました。

方法4. attrsライブラリを使う

from attrs import define, field

@define
class Gadget:

    name: str = field()
    price: int = field()
    brand: str = field()

    @name.validator
    def validate_name(self, attribute, value):
        if len(value) > 15:
            raise ValueError("name cannot exceed 15 characters.")
            
    @price.validator
    def validate_price(self, attribute, value):
        if value < 0:
            raise ValueError("Price cannot be negative.")


if __name__ == "__main__":
    MBP15 = Gadget(
        name="MacBookPro15",
        price=150000,
        brand="Apple"
    )
    TP = Gadget(
        name="ThinkPadX1Carbon",
        price=120000,
        brand="lenovo"
    )
    # ValueError: name cannot exceed 15 characters.
    foo = Gadget(
        name="Something",
        price=-111,
        brand=None
    )
    # ValueError: Price cannot be negative.

外部ライブラリattrを使う方法です。詳しくはライブラリのドキュメントを読むのが良いのですが、クラスの中にデコレータを作ってバリデーションを行うことができます。

ある程度シンプルに書けますが、デフォルトでも十分バリデーションはできると思ったので方法2,3よりは優先度下がるかなと思います。

終わりに

個人的には方法2, 3あたりが__init__もシンプルで良さそうだなと思いました。
どんどんバリデーションしてわかりやすいコードを心がけたいと思います。

参考

18
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?