【 Effective Python, 2nd Edition 】クラス作成時の setter メソッド、getter メソッドの利用は最小限に。可能な限り public 属性によるインターフェース構築を目指しましょう 投稿一覧へ戻る
Published 2020年7月15日21:14 by mootaro23
SUPPORT UKRAINE
- Your indifference to the act of cruelty can thrive rogue nations like Russia -
他の言語の使い手が Python を使うときに良くありがちなのが、クラスを定義する際に明示的に setter メソッドと getter メソッドを実装することです。
使用方法は簡単ですし、自己説明的です。
また、setter メソッド、getter メソッドを利用することで、異なるクラス間における同様のインターフェースの提供、内部操作のブラックボックス化、属性値を検証・操作するコードの実装のしやすさと読解性の向上、といった、クラス設計上の重要な項目をクリアできることは確かです。
そしてそれらの項目をクリアしていることで、外部からの呼び出しに影響を与えずに内部機能をアップデート可能、ということが保証されているんですね。
ただし Pythonic かと問えば...、残念ながら違います。
しかも外部から属性値に対してちょっとした操作を行いたい場合でも、コードは結構冗長です。
Python では、クラス設計開始時点から明示的に setter メソッド、getter メソッドを実装する必要はありません。
まずは通常通り public 属性の実装から始めましょう。
外部から属性値に対してちょっとした操作を行う場合でも、コードは簡潔ですし直感的です。
そして後になって属性値の割り当て時に何らかの操作を加えたくなったとしたら、@property 修飾詞 ( decorator ) を適用し、適用した属性に対する setter メソッドを同時に実装しましょう。
この実装では、money_with_me プロパティへの値の代入時には money_with_me の setter メソッドが呼ばれるようになります。
そうすることで、クラス内部では代入された値を元に内部の属性値を変更していますが、外部の利用者からすればただ単にクラスの属性値にアクセスしていることに変わりはありません。
setter メソッドを実装することで、値が代入される際に型チェックや検証を行えるようにもなります。
ここでは、商品価格は常に 0 円よりも高くなければいけない、というチェックを加えてみましょう。
不正な価格を入力すると例外が発生します。
コンストラクタ ( constructor ) に不正な価格を渡した場合にもエラーになります。
何故でしょう?
これは、ConfirmPrice.__init__() では親クラスの HowManyCanIBuy.__init__() を呼んでいて、そこでは self.price = -300 を実行します。
この price 変数に対する代入が ConfirmPrice クラス内で定義されている @price.setter を呼び出してチェックが実行された結果なんです。
さて、@property デコレータを利用して、親クラスの属性値を変更不可能 ( immutable ) にすることもできます。
クラスインスタンスを作成した後に price 属性を変更してみましょう。
最後に、@property デコレータ + setter or/and getter メソッドを利用する際の注意点です。
クラスを利用するユーザーはあくまでも属性値を取得、設定しているだけであり、それは非常に単純でパフォーマンス的にコストの低い操作だと思っているはずです。
もし setter メソッドや getter メソッドの中でパフォーマンス的にコストの高い I/O 操作やデータベースアクセス等を行っていれば、ユーザーの意に反して非常にレスポンスの悪いプログラムになってしまいます。
少しでもコストがかかる処理を行うのであれば、通常の関数として実装するようにすべきです。
まとめ:
class OldHowManyCanIBuy:
def __init__(self, price):
self._price = price
def get_price(self):
return self._price
def set_price(self, price):
self._price = price
def __init__(self, price):
self._price = price
def get_price(self):
return self._price
def set_price(self, price):
self._price = price
使用方法は簡単ですし、自己説明的です。
boo = OldHowManyCanIBuy(220)
print(f"変更前: {boo.get_price()} 円/個")
# 変更前: 220 円/個
boo.set_price(250)
print(f"変更後: {boo.get_price()} 円/個")
# 変更後: 250 円/個
print(f"変更前: {boo.get_price()} 円/個")
# 変更前: 220 円/個
boo.set_price(250)
print(f"変更後: {boo.get_price()} 円/個")
# 変更後: 250 円/個
また、setter メソッド、getter メソッドを利用することで、異なるクラス間における同様のインターフェースの提供、内部操作のブラックボックス化、属性値を検証・操作するコードの実装のしやすさと読解性の向上、といった、クラス設計上の重要な項目をクリアできることは確かです。
そしてそれらの項目をクリアしていることで、外部からの呼び出しに影響を与えずに内部機能をアップデート可能、ということが保証されているんですね。
ただし Pythonic かと問えば...、残念ながら違います。
しかも外部から属性値に対してちょっとした操作を行いたい場合でも、コードは結構冗長です。
boo.set_price(boo.get_price() - 25)
print(f"値切り交渉後: {boo.get_price()} 円/個")
# 値切り交渉後: 225 円/個
print(f"値切り交渉後: {boo.get_price()} 円/個")
# 値切り交渉後: 225 円/個
Python では、クラス設計開始時点から明示的に setter メソッド、getter メソッドを実装する必要はありません。
まずは通常通り public 属性の実装から始めましょう。
class HowManyCanIBuy:
def __init__(self, price):
self.price = price
self.money_with_me = 0
self.how_many_items = 0
def __init__(self, price):
self.price = price
self.money_with_me = 0
self.how_many_items = 0
外部から属性値に対してちょっとした操作を行う場合でも、コードは簡潔ですし直感的です。
foo = HowManyCanIBuy(230)
foo.price -= 30
print(f"値切り交渉後: {foo.price} 円/個")
# 値切り交渉後: 200 円/個
foo.price -= 30
print(f"値切り交渉後: {foo.price} 円/個")
# 値切り交渉後: 200 円/個
そして後になって属性値の割り当て時に何らかの操作を加えたくなったとしたら、@property 修飾詞 ( decorator ) を適用し、適用した属性に対する setter メソッドを同時に実装しましょう。
class ShoppingMango(HowManyCanIBuy):
def __init__(self, price):
super().__init__(price)
@property
def money_with_me(self):
return self._charge
@money_with_me.setter
def money_with_me(self, charge):
self._charge = charge
self.how_many_items = self._charge // self.price
def __init__(self, price):
super().__init__(price)
@property
def money_with_me(self):
return self._charge
@money_with_me.setter
def money_with_me(self, charge):
self._charge = charge
self.how_many_items = self._charge // self.price
この実装では、money_with_me プロパティへの値の代入時には money_with_me の setter メソッドが呼ばれるようになります。
そうすることで、クラス内部では代入された値を元に内部の属性値を変更していますが、外部の利用者からすればただ単にクラスの属性値にアクセスしていることに変わりはありません。
mango = ShoppingMango(270)
print(f"チャージ前: {mango.how_many_items} 個買えます")
# チャージ前: 0 個買えます
mango.money_with_me = 1500
print(f"チャージ後: {mango.how_many_items} 個買えます")
# チャージ後: 5 個買えます
print(f"チャージ前: {mango.how_many_items} 個買えます")
# チャージ前: 0 個買えます
mango.money_with_me = 1500
print(f"チャージ後: {mango.how_many_items} 個買えます")
# チャージ後: 5 個買えます
setter メソッドを実装することで、値が代入される際に型チェックや検証を行えるようにもなります。
ここでは、商品価格は常に 0 円よりも高くなければいけない、というチェックを加えてみましょう。
class ConfirmPrice(HowManyCanIBuy):
def __init__(self, price):
super().__init__(price)
@property
def price(self):
return self._item_price
@price.setter
def price(self, price):
if price <= 0:
raise ValueError(f"商品価格は 0 円よりも高くなければなりません: error (price={price})")
self._item_price = price
def __init__(self, price):
super().__init__(price)
@property
def price(self):
return self._item_price
@price.setter
def price(self, price):
if price <= 0:
raise ValueError(f"商品価格は 0 円よりも高くなければなりません: error (price={price})")
self._item_price = price
不正な価格を入力すると例外が発生します。
banana = ConfirmPrice(300)
banana.price = 0
# Traceback...
# ValueError: 商品価格は 0 円よりも高くなければなりません: error (price=0)
banana.price = 0
# Traceback...
# ValueError: 商品価格は 0 円よりも高くなければなりません: error (price=0)
コンストラクタ ( constructor ) に不正な価格を渡した場合にもエラーになります。
cherries = ConfirmPrice(-300)
# Traceback...
# ValueError: 商品価格は 0 円よりも高くなければなりません: error (price=-300)
# Traceback...
# ValueError: 商品価格は 0 円よりも高くなければなりません: error (price=-300)
何故でしょう?
これは、ConfirmPrice.__init__() では親クラスの HowManyCanIBuy.__init__() を呼んでいて、そこでは self.price = -300 を実行します。
この price 変数に対する代入が ConfirmPrice クラス内で定義されている @price.setter を呼び出してチェックが実行された結果なんです。
さて、@property デコレータを利用して、親クラスの属性値を変更不可能 ( immutable ) にすることもできます。
class FixedPrice(HowManyCanIBuy):
def __init__(self, price):
super().__init__(price)
@property
def price(self):
return self._item_price
@price.setter
def price(self, price):
if hasattr(self, '_item_price'):
raise AttributeError("price 属性は変更できません")
self._item_price = price
def __init__(self, price):
super().__init__(price)
@property
def price(self):
return self._item_price
@price.setter
def price(self, price):
if hasattr(self, '_item_price'):
raise AttributeError("price 属性は変更できません")
self._item_price = price
クラスインスタンスを作成した後に price 属性を変更してみましょう。
avocado = FixedPrice(350)
print(f"アボガド: {avocado.price} 円/個")
# アボガド: 350 円/個
avocado.price = 400
# Traceback...
# AttributeError: price 属性は変更できません
print(f"アボガド: {avocado.price} 円/個")
# アボガド: 350 円/個
avocado.price = 400
# Traceback...
# AttributeError: price 属性は変更できません
最後に、@property デコレータ + setter or/and getter メソッドを利用する際の注意点です。
クラスを利用するユーザーはあくまでも属性値を取得、設定しているだけであり、それは非常に単純でパフォーマンス的にコストの低い操作だと思っているはずです。
もし setter メソッドや getter メソッドの中でパフォーマンス的にコストの高い I/O 操作やデータベースアクセス等を行っていれば、ユーザーの意に反して非常にレスポンスの悪いプログラムになってしまいます。
少しでもコストがかかる処理を行うのであれば、通常の関数として実装するようにすべきです。
まとめ:
1: 新しいクラスを定義する場合は、最初から setter メソッドや getter メソッドを定義することはせず、通常の public 属性を用いた設計を行いましょう。
2: 属性へのアクセスがあった際に何らかの通常外の操作を行いたい場合だけ @property の利用を検討しましょう。
3: @property のメソッドで実行される操作がパフォーマンス的に悪い、もしくは、複雑なものである場合は、通常の関数として実装しましょう。
この記事に興味のある方は次の記事にも関心を持っているようです...
- People who read this article may also be interested in following articles ... -