#0140（2025/05/18）@propertyの解説と実践

# @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)`オブジェクトとして生成します。具体的には：

```python
# 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`の一種であり、内部的に以下のように動作します。

```python
class A:
    @property
    def x(self):
        return compute()
# この時点で A.x は property(fget=A.x, fset=None, fdel=None) となる
```

### 2.2.1 属性取得時の流れ（getter呼び出し）

1. `a = A()` によってインスタンスが生成される。
2. `a.x` と書くと、属性名`x`の解決が始まる。

   * Pythonはまずクラス`A`の辞書`A.__dict__`を調べ、`x`が`property`オブジェクトであることを確認。
   * `property`オブジェクトの`__get__(self, obj, objtype)`メソッドを呼び出す。
3. `property.__get__`内部では、登録してある`fget`（ここでは`A.x`の元メソッド）を呼び出す。

   ```python
   def __get__(self, obj, objtype=None):
       if obj is None:
           return self  # クラス参照時はプロパティオブジェクトを返す
       return self.fget(obj)  # インスタンスを渡してメソッド実行
   ```
4. `self.fget(obj)`により、`compute()`が実行され、その結果が返る。
5. 結果として`a.x`は`compute()`の戻り値となる。

### 2.2.2 属性代入時の動き（setter未定義）

1. `a.x = 10` と書くと、同様に属性名`x`の解決が始まる。
2. `property`オブジェクトに`fset`（setter関数）が定義されていないため、`property.__set__`は`None`を検出し、以下のようにエラーを投げる。

   ```python
   def __set__(self, obj, value):
       if self.fset is None:
           raise AttributeError("can't set attribute")
       return self.fset(obj, value)
   ```
3. その結果、`AttributeError: can't set attribute` が発生する。

---

## 3. getterのみの基本実装例と応用

### 3.1 基本例：遅延計算

```python
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`を使うと、初回アクセス後に結果をキャッシュし、以降は高速に返せます。

```python
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定義の基本構文

```python
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 実務への適用シナリオ

1. **入力バリデーション**：範囲・型チェックを一元管理し、ビジネスルールを保護。
2. **副作用トリガー**：属性変更時に他オブジェクトの更新やイベント通知を実行。
3. **依存プロパティの無効化**：派生プロパティのキャッシュクリア。

### 4.3 パフォーマンスとデバッグ

* setter内に重い処理を入れすぎると代入操作がボトルネックに。
* IDEによってはsetterの存在が補完候補に表示されず分かりにくい場合があるため、ドキュメンテーション文字列を充実させること。

---

## 5. @property.deleterの仕組みと利用パターン

### 5.1 deleter定義の基本構文

```python
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の活用

```python
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による宣言的定義

```python
from attrs import define, field

@define
class User:
    age: int = field(validator=[lambda inst, attr, v: v >= 0])
```

* 属性とバリデーションを一行で定義可能。
* 大規模モデルやAPIレスポンスの検証に強み。

### 6.3 \_\_setattr\_\_のオーバーライドによる一元管理

```python
class BaseModel:
    def __setattr__(self, key, val):
        # 共通バリデーションや通知を実装
        super().__setattr__(key, val)
```

* 全属性の設定を横断的に制御。
* 設計を誤るとデバッグ困難に。

---



## 7. まとめとベストプラクティス

* **目的に応じて**getter/setter/deleterを適切に使い分け、小粒なロジックを隠蔽・管理しよう。
* **パフォーマンス要件**が高い箇所では、過度なメソッド呼び出しを避けるか、`cached_property`などを併用。
* **属性数が増大**したらDescriptor／attrs／pydantic導入で記述量を削減し、可読性と保守性を両立。
* APIとして公開する属性の柔軟性を維持しながら、将来的なメソッド化も容易にするのが`@property`の真価です。