0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

#0140(2025/05/18)@propertyの解説と実践

Posted at

@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呼び出し)

  1. a = A() によってインスタンスが生成される。

  2. a.x と書くと、属性名xの解決が始まる。

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

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

  5. 結果としてa.xcompute()の戻り値となる。

2.2.2 属性代入時の動き(setter未定義)

  1. a.x = 10 と書くと、同様に属性名xの解決が始まる。

  2. 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)
    
  3. その結果、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 = -1ValueError
  • setter未定義時はAttributeError

4.2 実務への適用シナリオ

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

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の真価です。
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?