TL;DR
@property は『関数呼び出しのコストで属性アクセスの記法を提供する』仕組み(デスクリプタ)です。
- 読みやすい API を保ったままバリデーションや計算済み値を提供できる
-
@x.setter/@x.deleterで代入や削除時の振る舞いも制御できる - 重い計算は
functools.cached_propertyを検討(初回のみ計算しキャッシュ)
1. @property とは
@property はデスクリプタで実装されたビルトイン。obj.x という属性アクセスの裏で、実はメソッド呼び出しのように振る舞わせられます。
-
@property(getter)は読み取り時のフック -
@x.setterは代入時のフック -
@x.deleterはdel obj.x時のフック
外からはただの属性に見えるのに、内部では検証・計算・変換が可能。公開 API を変えずに内部実装を差し替えられます。
2. 使いどころ
- 入力値のバリデーション
- 計算プロパティ(例:
area,full_name) - 既存 API を壊さず内部を進化
- 遅延評価やメモ化は
cached_property
3. 基本構文:getter / setter / deleter
class Product:
def __init__(self, price: float) -> None:
self._price = price # 実体は慣例で '_' 付きに保持
@property
def price(self) -> float:
'現在の価格(常に 0 以上)'
return self._price
@price.setter
def price(self, value: float) -> None:
if value < 0:
raise ValueError('price must be non-negative')
self._price = float(value)
@price.deleter
def price(self) -> None:
raise AttributeError('price cannot be deleted')
4. 実践例
4-1. 読み取り専用の計算プロパティ
import math
class Circle:
def __init__(self, radius: float) -> None:
self._radius = radius
@property
def radius(self) -> float:
return self._radius
@radius.setter
def radius(self, r: float) -> None:
if r <= 0:
raise ValueError('radius must be positive')
self._radius = r
@property
def area(self) -> float:
'半径から面積を計算(読み取り専用)'
return math.pi * (self._radius ** 2)
4-2. 相互に整合する属性(正規化と不変条件)
class User:
def __init__(self, first: str, last: str) -> None:
self.first = first.strip()
self.last = last.strip()
@property
def full_name(self) -> str:
return f'{self.first} {self.last}'
@full_name.setter
def full_name(self, value: str) -> None:
first, last = value.split(maxsplit=1)
self.first, self.last = first.strip(), last.strip()
4-3. 代入時の自動変換(文字列 → 数値など)
class TemperatureC:
def __init__(self, celsius: float) -> None:
self._c = float(celsius)
@property
def celsius(self) -> float:
return self._c
@celsius.setter
def celsius(self, value) -> None:
self._c = float(value) # '36.5' のような文字列も受け付ける
@property
def fahrenheit(self) -> float:
return self._c * 9/5 + 32
4-4. 重い計算は cached_property
from functools import cached_property
class Report:
def __init__(self, rows: list[dict]) -> None:
self.rows = rows
@cached_property
def summary(self) -> dict:
# 初回アクセス時だけ集計し、結果をキャッシュ
return {
'count': len(self.rows),
'keys': sorted({k for row in self.rows for k in row})
}
r = Report([{'a': 1}, {'b': 2}])
_ = r.summary # 2回目以降はキャッシュを返す
# del r.summary # キャッシュを無効化して再計算
5. dataclasses と組み合わせる
from dataclasses import dataclass, field
@dataclass
class Item:
_price: float = field(repr=False) # 実体
name: str = 'unknown'
@property
def price(self) -> float:
return self._price
@price.setter
def price(self, v: float) -> None:
if v < 0:
raise ValueError('price must be >= 0')
self._price = float(v)
注意:
dataclassのフィールド名と同じ名前の@propertyを作らない。内部は_price、外はpriceに分ける。
6. 継承・抽象クラスでの使い方
from abc import ABC, abstractmethod
class Shape(ABC):
@property
@abstractmethod
def area(self) -> float:
'面積(サブクラスが実装)'
raise NotImplementedError
class Rectangle(Shape):
def __init__(self, w: float, h: float) -> None:
self.w, self.h = w, h
@property
def area(self) -> float:
return self.w * self.h
7. クラス属性に似たものが欲しい(classproperty)
class classproperty:
def __init__(self, fget):
self.fget = fget
def __get__(self, obj, owner):
return self.fget(owner) # cls を渡す
class Repo:
_default_url = 'https://example.com/api'
@classproperty
def default_url(cls) -> str:
return cls._default_url
Repo.default_url # クラスからアクセス可
Repo().default_url # インスタンスからも同じ
8. よくある落とし穴とベストプラクティス
- 重い処理を入れない(必要なら
cached_property) - 例外の選択:値域違反や不正値は
ValueError、型不一致はTypeError目安 - 初期化順序に注意:
__init__でセッター経由にするか方針を統一 - 無限再帰に注意:セッター内で再び
self.x = ...しない(内部実体self._xを直接操作) -
__repr__での副作用に注意 - スレッド安全性:一度きりの計算は競合しうる
補足:
propertyはデータ・デスクリプタとして動作するため、同名のインスタンス辞書の値で簡単に上書きされません(セッター未定義の代入はAttributeError)。