【Python】インスタンス属性の誤用:メソッド間で値を渡すためだけの属性はなぜ悪いのか?

クラスを設計する際、あるメソッドで計算した結果を、別のメソッドで使いたい、という状況は頻繁に発生します。このとき、それらの値を一時的に保持するために、__init__self.result = Noneのようにインスタンス属性を初期化し、メソッド間で受け渡しする、という実装をしてしまいがちです。

しかし、このアプローチは**「時間的結合(Temporal Coupling)」**と呼ばれる悪しき設計パターンを生み出し、クラスを非常に壊れやすく、使いにくいものにしてしまいます。今回は、この問題点と、Pythonの@propertyを使ったモダンで堅牢な解決策を見ていきましょう。

目次

問題点:メソッド呼び出しの「順序」に依存するクラス

ある商品の価格と割引率から、最終的な販売価格と割引額を表示するクラスを考えてみましょう。

悪い例:self.final_priceを中間データ保持のために使っている

class PriceCalculator:
    def __init__(self, base_price: float, discount_rate: float):
        self.base_price = base_price
        self.discount_rate = discount_rate
        self.final_price = None # 中間データを保持するための属性
        self.discount_amount = None # これも同様

    def calculate(self):
        """価格を計算し、結果をインスタンス属性にセットする。"""
        self.discount_amount = self.base_price * self.discount_rate
        self.final_price = self.base_price - self.discount_amount
        print("(計算が実行されました)")

    def display(self):
        """計算結果を表示する。"""
        # このメソッドは、calculate()が先に呼ばれていることを前提としている
        if self.final_price is None:
            print("エラー: 先に .calculate() を呼び出してください。")
            return
        
        print(f"割引額: {self.discount_amount}円, 販売価格: {self.final_price}円")

# 実行例
calc = PriceCalculator(1000, 0.2)
# calc.display() # -> 先に呼び出すとエラーになる
calc.calculate() # こちらを先に呼び出す必要がある
calc.display()

このPriceCalculatorクラスには、致命的な設計上の問題があります。display()メソッドが正しく動作するためには、その前に必ずcalculate()メソッドが呼び出されていなければなりません。このような**「メソッド呼び出しの順序に依存する」**関係を「時間的結合」と呼びます。

これにより、クラスの利用者は常に正しい順序を意識する必要があり、非常に使いにくく、予期せぬバグ(calculate()の呼び忘れなど)の原因となります。


解決策①:@propertyで「計算」を「属性アクセス」に見せる

この問題を解決する最もエレガントな方法は、計算ロジックを@propertyデコレータを持つメソッドに実装することです。@propertyを使うと、メソッドをインスタンス属性のように(()なしで)呼び出すことができます。

良い例:@propertyで派生データをその都度計算する

class PriceCalculator:
    def __init__(self, base_price: float, discount_rate: float):
        self.base_price = base_price
        self.discount_rate = discount_rate
        # 中間データを保持する属性は不要になった!

    @property
    def discount_amount(self) -> float:
        """割引額を計算して返す。"""
        return self.base_price * self.discount_rate

    @property
    def final_price(self) -> float:
        """最終的な販売価格を計算して返す。"""
        return self.base_price - self.discount_amount

    def display(self):
        """計算結果を表示する。"""
        # 呼び出し順序を気にする必要がない
        print(f"割引額: {self.discount_amount}円, 販売価格: {self.final_price}円")

# 実行例
calc = PriceCalculator(1000, 0.2)
calc.display() # いつ呼び出しても正しく動作する

final_pricediscount_amountは、base_pricediscount_rateという**根源的なデータから導出される「派生データ」**です。@propertyを使うことで、これらの派生データはアクセスされるたびにリアルタイムで計算されるようになり、中間的なインスタンス属性は一切不要になりました。

これにより時間的結合は完全に解消され、利用者はもはやメソッドの呼び出し順序を気にする必要はありません。


解決策②:@cached_propertyで高コストな計算を効率化する

@propertyの唯一の懸念点は、アクセスされるたびに計算が実行されることです。もし計算が非常に重い(例:複雑なデータ分析や外部APIへの問い合わせ)場合、パフォーマンスに影響が出る可能性があります。

このようなケースのために、Python 3.8からはfunctools.cached_propertyが導入されました。これは、最初のアクセス時に一度だけ計算を実行し、その結果をインスタンスにキャッシュ(保存)してくれるデコレータです。

さらに良い例:@cached_propertyで初回のみ計算を実行

from functools import cached_property

class PriceCalculator:
    def __init__(self, base_price: float, discount_rate: float):
        self.base_price = base_price
        self.discount_rate = discount_rate

    @cached_property
    def discount_amount(self) -> float:
        print("(割引額を一度だけ計算中...)")
        return self.base_price * self.discount_rate

    @property
    def final_price(self) -> float: # こちらは単純なのでpropertyのまま
        return self.base_price - self.discount_amount

# 実行例
calc = PriceCalculator(1000, 0.2)
print(calc.discount_amount) # ここで計算が実行される
print(calc.discount_amount) # 2回目以降はキャッシュされた値が即座に返る

実行結果:

(割引額を一度だけ計算中...)
200.0
200.0

@cached_propertyは、@propertyの「いつでも最新」という利点と、インスタンス属性の「効率性」という利点を両立させる、非常に強力なツールです。


まとめ

クラスの状態管理をクリーンに保つための原則は、**「派生データは属性として保持せず、必要な時に計算する」**ことです。

  • メソッド間の値の受け渡しのためだけに、インスタンス属性を定義しない。
  • 単純で軽量な派生データには@propertyを使い、常に最新の値を返す。
  • 計算コストが高い派生データには@cached_propertyを使い、初回アクセス時にのみ計算を実行する。

このアプローチにより、メソッドの呼び出し順序に依存しない、堅牢で直感的に使えるクラスを設計することができます。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次