Python|propertyで副作用設計ミスを起こさない考え方
この記事のポイント
- 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.0propertyにより「関数呼び出しなのに属性参照のように使える」柔軟さが得られます。ただし柔軟すぎるが故に設計ミスの温床にもなります。
なぜこれをレビューするのか
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
@Reviewergetter内で属性変更を伴う副作用を発生させています。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")
@Reviewergetter内で外部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)
@Reviewersetter内で複数責務を混在させています。状態更新と通知設計は分離してください。副作用設計が膨張しています。
問題点
- 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()
@Reviewergetter内でキャッシュ管理責任まで背負っています。評価遅延とキャッシュ責任を統一設計すべきです。 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設計イメージ
観点チェックリスト
まとめ
propertyは「便利で危険な糖衣構文」です。レビューアーは常に状態変更と計算参照の分離原則を読み取り、呼び出し順序依存・副作用設計肥大・外部I/O混入を静かに防止していく必要があります。property肥大は「責務不明確化のサイン」と読み取るのがレビュー技術のコツです。

