一覧PG-13-2 · オブジェクトとクラス

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

1
class PlainQuantity: def __init__(self, value): if value < 0: raise ValueError("negative quantity") self.quantity = value
2
plain = PlainQuantity(3); plain.quantity = -1; print(plain.quantity)
-1
3
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 = value
4
q = Quantity(3); q.quantity = 5; print(q.quantity, vars(q))
5 {'_quantity': 5}
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
6
q.quantity = 0; print(q.quantity)
0
7
try: invalid = Quantity(-2) except ValueError as error: print(type(error).__name__, str(error))
ValueError quantity must be nonnegative
8
q._quantity = -7; print(q.quantity)
-7
9
q.quantity = 2; print(q.quantity, vars(q))
2 {'_quantity': 2}

演習完了