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?

Pythonの @property 徹底ガイド ─ 属性アクセスの顔をしたメソッド

Last updated at Posted at 2025-10-24

TL;DR

@property は『関数呼び出しのコストで属性アクセスの記法を提供する』仕組み(デスクリプタ)です。

  • 読みやすい API を保ったままバリデーションや計算済み値を提供できる
  • @x.setter / @x.deleter で代入や削除時の振る舞いも制御できる
  • 重い計算は functools.cached_property を検討(初回のみ計算しキャッシュ)

1. @property とは

@property はデスクリプタで実装されたビルトイン。obj.x という属性アクセスの裏で、実はメソッド呼び出しのように振る舞わせられます。

  • @property(getter)は読み取り時のフック
  • @x.setter は代入時のフック
  • @x.deleterdel 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)。

参考

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?