検索ガイド -Search Guide-

単語と単語を空白で区切ることで AND 検索になります。
例: python デコレータ ('python' と 'デコレータ' 両方を含む記事を検索します)
単語の前に '-' を付けることで NOT 検索になります。
例: python -デコレータ ('python' は含むが 'デコレータ' は含まない記事を検索します)
" (ダブルクオート) で語句を囲むことで 完全一致検索になります。
例: "python data" 実装 ('python data' と '実装' 両方を含む記事を検索します。'python data 実装' の検索とは異なります。)
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
>>
effective_python

【 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 メソッドを実装することです。


class OldHowManyCanIBuy:
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 円/個



また、setter メソッド、getter メソッドを利用することで、異なるクラス間における同様のインターフェースの提供、内部操作のブラックボックス化、属性値を検証・操作するコードの実装のしやすさと読解性の向上、といった、クラス設計上の重要な項目をクリアできることは確かです。


そしてそれらの項目をクリアしていることで、外部からの呼び出しに影響を与えずに内部機能をアップデート可能、ということが保証されているんですね。


ただし Pythonic かと問えば...、残念ながら違います。
しかも外部から属性値に対してちょっとした操作を行いたい場合でも、コードは結構冗長です。


boo.set_price(boo.get_price() - 25)

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



外部から属性値に対してちょっとした操作を行う場合でも、コードは簡潔ですし直感的です。


foo = HowManyCanIBuy(230)
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



この実装では、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 個買えます



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



不正な価格を入力すると例外が発生します。


banana = ConfirmPrice(300)
banana.price = 0
# Traceback...
# ValueError: 商品価格は 0 円よりも高くなければなりません: error (price=0)



コンストラクタ ( constructor ) に不正な価格を渡した場合にもエラーになります。


cherries = ConfirmPrice(-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



クラスインスタンスを作成した後に price 属性を変更してみましょう。


avocado = FixedPrice(350)

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 ... -
【 Effective Python, 2nd Edition 】Python においてクラス属性に厳密な private が無いのは何故? できる限り利用すべきではない理由とは? それでも private の使用が有効な状況とは?
【 Effective Python, 2nd Edition 】__getattr__、__getattribute__、__setattr__ メソッドを利用して、事前に定義していないインスタンス属性を操作しよう!
【 Effective Python, 2nd Edition 】Queue クラスを利用した producer-consumer パイプライン ( pipelines ) を構築して、マルチスレッドシーケンス処理をエレガントに管理しよう! 並行実行 ( parallelism ) と並列処理 ( concurrency ) もついでにちゃんとイメージしよう!
【 Effective Python, 2nd Edition 】組み込みタイプ ( built-in types ) を利用していてネストが深くなってきたらクラス ( class ) を作成する頃合いです、の巻
【 Effective Python, 2nd Edition 】デスクリプタ ( descriptor ) を利用して @property で行っていた属性値への操作を再利用できるようにしよう!
【 Effective Python, 2nd Edition 】プログラムを並列処理 ( concurrency ) パターンへ移行するタイミングとツールを考えるシリーズ 第 3 回 - Thread インスタンスの頻繁な start / join による fan-out / fan-in パターン実装は避けるべし、の巻
【Python 雑談・雑学】メタクラス ( metaclass ) とデコレータ ( decorator ) で遊んでみる、考えてみる!