@propertyの解説と実践
1. はじめに
Pythonでは、クラス設計において内部実装の隠蔽(情報隠蔽)とAPIの後方互換性を両立させることが重要です。特にライブラリやフレームワークの開発では、公開する属性を後からメソッドに切り替えても既存コードを壊さずにバージョンアップしたいケースが多々あります。本記事では、Python標準の@property
デコレーターを中心に、内部で動いているdescriptorプロトコルの詳細や、実務で役立つパターン・パフォーマンス上の注意点、さらにはsetter
/deleter
の使いどころ、冗長さを抑える技術まで、上級エンジニア視点で深掘りします。
2. @propertyの仕組みと概要
2.1 descriptorプロトコルの理解
Pythonの属性アクセスは、ただの辞書引きではなく、以下の3つのマジックメソッドを介します。
-
__get__(self, obj, objtype)
:属性取得時に呼び出し -
__set__(self, obj, value)
:属性代入時に呼び出し -
__delete__(self, obj)
:del obj.attr
時に呼び出し
これらをまとめて扱うのがdescriptorという仕組みで、@property
は内部的に三つのハンドラをproperty(fget, fset, fdel)
オブジェクトとして生成します。具体的には:
# fget: getter関数
# fset: setter関数 or None
# fdel: deleter関数 or None
descriptor = property(fget=getter, fset=setter, fdel=deleter)
2.2 @propertyデコレーターの動作
Pythonはインスタンスの属性にアクセスする際、descriptorの__get__
メソッドを呼び出します。@property
で作成されたproperty
オブジェクトはdescriptor
の一種であり、内部的に以下のように動作します。
class A:
@property
def x(self):
return compute()
# この時点で A.x は property(fget=A.x, fset=None, fdel=None) となる
2.2.1 属性取得時の流れ(getter呼び出し)
-
a = A()
によってインスタンスが生成される。 -
a.x
と書くと、属性名x
の解決が始まる。- Pythonはまずクラス
A
の辞書A.__dict__
を調べ、x
がproperty
オブジェクトであることを確認。 -
property
オブジェクトの__get__(self, obj, objtype)
メソッドを呼び出す。
- Pythonはまずクラス
-
property.__get__
内部では、登録してあるfget
(ここではA.x
の元メソッド)を呼び出す。def __get__(self, obj, objtype=None): if obj is None: return self # クラス参照時はプロパティオブジェクトを返す return self.fget(obj) # インスタンスを渡してメソッド実行
-
self.fget(obj)
により、compute()
が実行され、その結果が返る。 -
結果として
a.x
はcompute()
の戻り値となる。
2.2.2 属性代入時の動き(setter未定義)
-
a.x = 10
と書くと、同様に属性名x
の解決が始まる。 -
property
オブジェクトにfset
(setter関数)が定義されていないため、property.__set__
はNone
を検出し、以下のようにエラーを投げる。def __set__(self, obj, value): if self.fset is None: raise AttributeError("can't set attribute") return self.fset(obj, value)
-
その結果、
AttributeError: can't set attribute
が発生する。
3. getterのみの基本実装例と応用
3.1 基本例:遅延計算
class Circle:
def __init__(self, r: float):
self._r = r
@property
def area(self) -> float:
# 毎回計算される
return math.pi * self._r ** 2
- 特徴:
c.area
と書くだけで都度計算。内部実装を隠蔽でき、後からキャッシュロジックを追加しやすい。
3.2 応用:キャッシュ化とfunctools.cached_property
標準ライブラリのfunctools.cached_property
を使うと、初回アクセス後に結果をキャッシュし、以降は高速に返せます。
from functools import cached_property
class Document:
@cached_property
def parsed(self) -> ParsedTree:
return expensive_parse(self.text)
- 初回呼び出し:
expensive_parse
実行、以降はメモリ上の値参照。 - 注意:キャッシュ無効化には明示的に
del doc.parsed
が必要。
4. @property.setterの仕組みと利用パターン
4.1 setter定義の基本構文
class User:
@property
def age(self) -> int:
return self._age
@age.setter
def age(self, v: int):
if v < 0:
raise ValueError("age must be >= 0")
self._age = v
-
u.age = -1
→ValueError
- setter未定義時は
AttributeError
4.2 実務への適用シナリオ
- 入力バリデーション:範囲・型チェックを一元管理し、ビジネスルールを保護。
- 副作用トリガー:属性変更時に他オブジェクトの更新やイベント通知を実行。
- 依存プロパティの無効化:派生プロパティのキャッシュクリア。
4.3 パフォーマンスとデバッグ
- setter内に重い処理を入れすぎると代入操作がボトルネックに。
- IDEによってはsetterの存在が補完候補に表示されず分かりにくい場合があるため、ドキュメンテーション文字列を充実させること。
5. @property.deleterの仕組みと利用パターン
5.1 deleter定義の基本構文
class Cache:
@property
def data(self) -> Dict[str, Any]:
return self._store
@data.deleter
def data(self):
self._store.clear()
-
del cache.data
→ キャッシュクリア
5.2 利用シーン
- メモリ/ファイルキャッシュの一括解放
- 外部リソース(DB接続、ファイルハンドル、ソケット)のクリーンアップ
- テスト環境でのリソース再設定:状態リセット用フックとして
5.3 注意点
-
del
後に再アクセスするとAttributeError
。再生成ロジックを検討。 - デフォルトでは
__init__
で再設定されない限り属性自体が消える点に留意。
6. 冗長化への対応策と高度パターン
大量のプロパティを持つ場合、getter/setter/deleterの定義が煩雑になります。以下の技術で共通化しましょう。
6.1 カスタムDescriptorの活用
class Validated:
def __init__(self, name, validator):
self.storage = '_' + name
self.validator = validator
def __get__(self, obj, cls):
return getattr(obj, self.storage)
def __set__(self, obj, val):
if not self.validator(val):
raise ValueError(f"Invalid value for {self.storage}")
setattr(obj, self.storage, val)
- 利点:バリデータを引数化し、一箇所で共通バリデーション。
- 注意:複雑なgetterロジックには向かない。
6.2 attrs / pydanticによる宣言的定義
from attrs import define, field
@define
class User:
age: int = field(validator=[lambda inst, attr, v: v >= 0])
- 属性とバリデーションを一行で定義可能。
- 大規模モデルやAPIレスポンスの検証に強み。
6.3 __setattr__のオーバーライドによる一元管理
class BaseModel:
def __setattr__(self, key, val):
# 共通バリデーションや通知を実装
super().__setattr__(key, val)
- 全属性の設定を横断的に制御。
- 設計を誤るとデバッグ困難に。
7. まとめとベストプラクティス
- 目的に応じてgetter/setter/deleterを適切に使い分け、小粒なロジックを隠蔽・管理しよう。
-
パフォーマンス要件が高い箇所では、過度なメソッド呼び出しを避けるか、
cached_property
などを併用。 - 属性数が増大したらDescriptor/attrs/pydantic導入で記述量を削減し、可読性と保守性を両立。
- APIとして公開する属性の柔軟性を維持しながら、将来的なメソッド化も容易にするのが
@property
の真価です。