この記事のポイント

  • Pythonのproperty設計で副作用肥大を起こさない技法を整理
  • 「計算」と「状態変更」の分離原則をレビューアー視点で読み解く
  • キャッシュ・遅延評価・状態整合性の実務的設計パターンを整理

そもそもpropertyとは

Pythonのpropertyは、属性アクセスの表現を隠蔽する設計補助機能です。

property基本例
class Product:
    def __init__(self, price, tax_rate):
        self._price = price
        self._tax_rate = tax_rate

    @property
    def total_price(self):
        return self._price * (1 + self._tax_rate)

p = Product(100, 0.1)
print(p.total_price)  # 110.0

propertyにより「関数呼び出しなのに属性参照のように使える」柔軟さが得られます。ただし柔軟すぎるが故に設計ミスの温床にもなります。

なぜこれをレビューするのか

propertyは非常に紛れやすい構造です。安易な設計は次の設計肥大を生みます。

  • 読み取りアクセスに副作用を仕込んでしまう
  • 遅延評価とキャッシュ責任が混在する
  • setterで副作用発火順序依存が発生する
  • 一見読み取り専用に見えて裏で複雑な状態変化が走る

レビューアーは「属性のように見えて何が走っているか」を常に読解しなければなりません。

レビューアー視点

  • propertyは参照系責務に限定されているか
  • getter内でI/Oや外部依存を発生させていないか
  • setterは単純代入責務に限定されているか
  • 遅延計算・キャッシュは明示設計されているか
  • 参照順序による不定挙動が発生し得ないか

開発者視点

propertyは「糖衣構文」と割り切り、状態変更は原則専用メソッドに分離する設計姿勢が重要です。


良い実装例

正常設計例:純粋参照責務
class Order:
    def __init__(self, items):
        self.items = items  # list of (price, qty)

    @property
    def total_price(self):
        return sum(price * qty for price, qty in self.items)

order = Order([(100, 2), (200, 1)])
print(order.total_price)  # 400

良い理由

  • getterは状態から純粋計算のみ
  • 外部I/O・状態変更は伴わない
  • 呼び出し回数による副作用が発生しない

レビュー観点

  • getterで状態依存の変動・副作用は発生しないか
  • setterでロジック分岐を行っていないか
  • キャッシュ設計は明示的に分離管理しているか
  • 読み取り時のI/O発生有無
  • 属性名が計算と状態混在で誤解を生んでいないか

良くない実装例: ケース1

問題例: getter副作用混入
class Order:
    def __init__(self, items):
        self.items = items
        self._access_count = 0

    @property
    def total_price(self):
        self._access_count += 1
@Reviewer
getter内で属性変更を伴う副作用を発生させています。getterは純粋計算責務に限定しましょう。
return sum(price * qty for price, qty in self.items)

問題点

  • 属性アクセスによる状態変化(アクセス回数蓄積)
  • 呼び出し順序により全体状態が変動

改善例

改善後: 副作用排除
class Order:
    @property
    def total_price(self):
        return sum(price * qty for price, qty in self.items)

# アクセス回数監視が必要なら外部で計測責務と分離すべき
設計補足
  • 状態監視・モニタリング系はobserver設計等で外出し
  • getter内はあくまで「関数のように安全に」設計する

良くない実装例: ケース2

問題例: getter内I/O依存
import requests

class ExchangeRate:
    @property
    def usd_to_jpy(self):
        response = requests.get("https://api.exchangerate.example/usd-jpy")
@Reviewer
getter内で外部I/Oを伴っています。呼び出しタイミングにより結果不定となりレビュー不能化します。I/Oは明示的メソッドに分離しましょう。
return response.json()["rate"]

問題点

  • 毎回外部API通信が発生
  • 状態把握不能
  • 呼び出し順序依存化

改善例

改善後: I/Oは更新責任に分離
class ExchangeRate:
    def __init__(self):
        self._rate = None

    def refresh(self):
        response = requests.get("https://api.exchangerate.example/usd-jpy")
        self._rate = response.json()["rate"]

    @property
    def usd_to_jpy(self):
        return self._rate
設計補足
  • 状態更新責任と参照責任の完全分離
  • 参照は常に安全かつ決定論的に利用可能

良くない実装例: ケース3

問題例: setter副作用肥大
class Person:
    def __init__(self, name):
        self._name = name

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        if value != self._name:
            log_name_change(self._name, value)
            self._name = value
            notify_observers(value)
@Reviewer
setter内で複数責務を混在させています。状態更新と通知設計は分離してください。副作用設計が膨張しています。

問題点

  • setter内でログ・通知・状態更新が混在
  • 設計責務が交差しレビュー不能化

改善例

改善後: setter純化と通知責務分離
class Person:
    def set_name(self, value):
        if value != self._name:
            self._name = value
            self.notify_change(value)

    def notify_change(self, new_value):
        log_name_change(self._name, new_value)
        notify_observers(new_value)
設計補足
  • setter=代入専任
  • 監視・通知・ログは別設計責任へ移譲

良くない実装例: ケース4(キャッシュ設計崩壊)

問題例: lazy評価とキャッシュ混同
class Computation:
    def __init__(self):
        self._result = None

    @property
    def expensive_result(self):
        if self._result is None:
            self._result = self._expensive_computation()
@Reviewer
getter内でキャッシュ管理責任まで背負っています。評価遅延とキャッシュ責任を統一設計すべきです。
return self._result def _expensive_computation(self): # 非常に重い計算 return 42

問題点

  • getterが評価遅延・キャッシュ管理の両方を混在
  • 無限肥大可能性
  • 初回評価時の例外設計不透明化

改善例

改善後: 明示キャッシュ管理責任
class Computation:
    def __init__(self):
        self._result = None

    def compute(self):
        self._result = self._expensive_computation()

    @property
    def expensive_result(self):
        if self._result is None:
            raise ValueError("未計算です")
        return self._result
設計補足
  • 遅延計算 vs キャッシュ責任を別途明示
  • getterは純粋参照責任だけを担保

PlantUML設計イメージ

UML Diagram
UML Diagram

観点チェックリスト


まとめ

propertyは「便利で危険な糖衣構文」です。レビューアーは常に状態変更と計算参照の分離原則を読み取り、呼び出し順序依存・副作用設計肥大・外部I/O混入を静かに防止していく必要があります。property肥大は「責務不明確化のサイン」と読み取るのがレビュー技術のコツです。