propertyによる属性の検証
個数を持つオブジェクトを使い、直接代入とpropertyによる更新を比較します。初期化と更新で同じ検査を使い、拒否した後の値も確認します。最後に、内部属性を直接操作した場合の限界を扱います。
説明
数量に負数が入ると、後の計算が正しくても在庫や料金の意味が崩れます。生成時に一度確認するだけでは、生成後の代入で不正な状態へ変わる可能性があります。
個数を持つオブジェクトを使い、直接代入とpropertyによる更新を比較します。初期化と更新で同じ検査を使い、拒否した後の値も確認します。最後に、内部属性を直接操作した場合の限界を扱います。
1生成時だけ検査するクラス
初期化時に負数を拒否するだけのクラスを定義します。
class PlainQuantity:直接属性を持つクラス
def __init__(self, value):
if value < 0:生成時の検査
raise ValueError("negative quantity")
self.quantity = value通常の属性へ保存__init__の検査は生成時にだけ実行されます。quantityへの後からの代入を監視する仕組みは、このクラスにはありません。
2生成後の代入
個数3で生成してから-1を代入します。
plain = PlainQuantity(3)有効な値で生成; plain.quantity = -1生成後に直接代入; print(plain.quantity)保存された値-1がそのまま保存されます。生成時の検査が、後からのすべての代入へ自動適用されるわけではありません。
3読み書きにpropertyを使う
quantityの読み取りと代入に関数を割り当てます。内部の保存先は_quantityです。
class Quantity:
def __init__(self, value):
self.quantity = value初期化もsetterを通す
@property読取り時の処理
def quantity(self):
return self._quantity
@quantity.setter代入時の処理
def quantity(self, value):
if type(value) is not int整数型だけを許可:
raise TypeError("quantity must be int")
if value < 0負数を拒否:
raise ValueError("quantity must be nonnegative")
self._quantity = value検査後に内部値を更新読み取りは@propertyの関数、代入は@quantity.setterの関数を通ります。__init__もself.quantityへ代入するため同じ検査が動きます。型の検査でboolを含むint以外を拒否し、負数を拒否した後に保存します。
4有効な更新
個数3で生成し、5へ更新します。属性辞書も確認します。
q = Quantity(3)検査して生成; q.quantity = 5setterで更新; print(q.quantity, vars(q)公開値と内部保存先)利用側はq.quantityという同じ表記で読み書きしますが、保存されている名前は_quantityです。propertyは公開する操作と内部の保存方法を分けられます。
5拒否した後の状態
負数、文字列、Trueを順に代入し、拒否後の個数を確認します。
for value in (-1, "2", True):三種類の不正値
try:
q.quantity = value同じ代入窓口へ渡す
except (TypeError, ValueError)検査失敗を受け取る as error:
print(repr(value), type(error).__name__, q.quantity拒否後の個数)いずれも拒否され、個数は5のままです。検査を保存より先に行っているため、失敗時に元の状態を保てます。TrueはPythonではintの派生型ですが、ここでは個数として許可していません。
60を有効な値として扱う
空の状態を表す0を代入します。
q.quantity = 0個数0へ更新; print(q.quantity)受理された値0は整数かつ非負なので受理されます。if not valueで検査すると、有効な0も拒否される場合があります。ここでは型と値の範囲をそれぞれ検査します。
7生成時にも同じ検査
不正な初期値で生成し、例外を確認します。
try:
invalid = Quantity(-2)不正な初期値を渡す
except ValueError初期化の失敗を確認 as error:
print(type(error).__name__, str(error))初期化でもsetterを通るため、負数を持つインスタンスを正常な戻り値として受け取りません。生成時と更新時に別々の検査を書かずに済みます。
8内部属性への直接代入
内部用の_quantityへ直接代入した場合を確認します。
q._quantity = -7setterを通らず内部値を更新; print(q.quantity)検査を迂回した状態-7が表示されます。先頭の下線は内部用という慣習であり、代入を禁止する機能ではありません。propertyの検査が働くのはquantity経由の更新です。利用側が内部保存先を直接変更しない設計と運用が必要です。
9公開された更新方法で戻す
quantityへ有効な個数を代入し、検査を通った状態へ戻します。
q.quantity = 2setterを通して修正; print(q.quantity, vars(q)修正後の状態)公開された窓口では再び検査が働き、2が保存されます。検査の有無は属性名の見た目ではなく、更新がどの処理を通るかで決まります。
まとめ
生成時の検査だけでは、生成後の代入まで検査できません。propertyで読取り・更新の処理を定義できます。
検査を保存より前に行うと、拒否したときに元の値を保てます。
内部属性の直接操作はpropertyの検査を通りません。利用側へ公開する更新方法を明確にします。
操作履歴と解説を振り返れます。
Python 3
class PlainQuantity:
def __init__(self, value):
if value < 0:
raise ValueError("negative quantity")
self.quantity = valueplain = PlainQuantity(3); plain.quantity = -1; print(plain.quantity)-1
class Quantity:
def __init__(self, value):
self.quantity = value
@property
def quantity(self):
return self._quantity
@quantity.setter
def quantity(self, value):
if type(value) is not int:
raise TypeError("quantity must be int")
if value < 0:
raise ValueError("quantity must be nonnegative")
self._quantity = valueq = Quantity(3); q.quantity = 5; print(q.quantity, vars(q))5 {'_quantity': 5}for value in (-1, "2", True):
try:
q.quantity = value
except (TypeError, ValueError) as error:
print(repr(value), type(error).__name__, q.quantity)-1 ValueError 5 '2' TypeError 5 True TypeError 5
q.quantity = 0; print(q.quantity)0
try:
invalid = Quantity(-2)
except ValueError as error:
print(type(error).__name__, str(error))ValueError quantity must be nonnegative
q._quantity = -7; print(q.quantity)-7
q.quantity = 2; print(q.quantity, vars(q))2 {'_quantity': 2}演習完了