はじめに
「良いコード/悪いコードで学ぶ設計入門」を読んでから、値のバリデーションについて意識するようになりました。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__
もシンプルで良さそうだなと思いました。
どんどんバリデーションしてわかりやすいコードを心がけたいと思います。