閉じる

@propertyの使い方まとめ(Python) ゲッター・セッターを簡潔に

クラスの属性に直接アクセスすると楽ですが、後から仕様変更やバリデーションが必要になった時に困ることがあります。

@propertyは、見た目は属性アクセスのまま、中身は関数として制御できる仕組みです。

初心者でも段階的に理解できるよう、基礎から実践、ベストプラクティスまで丁寧に解説します。

@propertyとは

ゲッターとセッターをPythonで簡潔に

Pythonでは@propertyデコレータを使うと、属性の取得や更新の裏で関数を実行できるようになります。

従来のget_x()set_x()のようなメソッド名を使わず、見た目は単なる属性アクセスのまま安全な読み書きを実装できます。

プロパティの利点とカプセル化

カプセル化とは、オブジェクトの内部表現を隠し、外部からの不正な操作を防ぐ設計です。

プロパティを使うと次の利点があります。

メソッドに変更しても外部APIは属性のままなので、既存コードの互換性が高いこと、値の検査や変換、ログ記録などを透過的に差し込めること、後片付けや遅延計算を簡単に仕込めることです。

直接アクセスとの違い

属性へ直接アクセスする場合は制御が効きません。

一方、プロパティなら、取得時や設定時に関数が呼ばれるため、バリデーションや変換、エラーメッセージによる誘導が可能です。

比較を以下にまとめます。

アプローチ記法の見た目バリデーション互換性維持実装の手間
直接アクセスobj.xできない低い低い
明示的メソッド(get/set)obj.get_x() / obj.set_x(v)できる低い(呼び方が変わる)
@propertyobj.x / obj.x = vできる高い(見た目は属性のまま)
注意

プロパティは便利ですが、重い処理を隠すとパフォーマンス問題に気づきにくくなります。

Pythonの@propertyの基本

読み取り専用プロパティの定義

ゲッターだけを定義すると読み取り専用になります。

内部保存用の属性は_nameのようなバッキングフィールドを使います。

Python
class Person:
    def __init__(self, first_name: str, last_name: str) -> None:
        # バッキングフィールドに実データを保持
        self._first_name = first_name
        self._last_name = last_name

    @property
    def full_name(self) -> str:
        """名と姓から表示用の氏名を返します(読み取り専用)。"""
        # 取得時に計算して返す(計算プロパティ)
        return f"{self._first_name} {self._last_name}"

p = Person("Taro", "Yamada")
print(p.full_name)   # 読み取りは可能
# p.full_name = "X"  # 書き込みはエラー(AttributeError)
実行結果
Taro Yamada

setterの追加と値の更新

@プロパティ名.setterでセッターを追加します。

値の検査や変換を行い、不正な値にはValueErrorTypeErrorを投げると、使い方を誤った呼び出し側へ明確に伝えられます。

Python
class Temperature:
    def __init__(self, celsius: float) -> None:
        self._celsius = float(celsius)  # バッキングフィールド

    @property
    def celsius(self) -> float:
        """摂氏温度を返します。"""
        return self._celsius

    @celsius.setter
    def celsius(self, value: float) -> None:
        """摂氏温度を設定します。絶対零度未満は不可です。"""
        # バリデーションを実施
        if not isinstance(value, (int, float)):
            raise TypeError("celsiusは数値で指定してください")
        if value < -273.15:
            raise ValueError("celsiusは-273.15以上である必要があります")
        self._celsius = float(value)

t = Temperature(0)
print(t.celsius)
t.celsius = 100
print(t.celsius)
実行結果
0.0
100.0

deleterの追加と後片付け

@プロパティ名.deleterでデリータを定義すると、del obj.prop時の挙動を制御できます。

キャッシュの破棄や外部リソースの解放を入れることができます。

Python
class CacheHolder:
    def __init__(self) -> None:
        self._data = [1, 2, 3]
        self._sum_cache = None  # 計算結果のキャッシュ

    @property
    def total(self) -> int:
        """配列の合計値(キャッシュあり)。"""
        if self._sum_cache is None:
            # 高コスト計算を想定
            self._sum_cache = sum(self._data)
        return self._sum_cache

    @total.deleter
    def total(self) -> None:
        """キャッシュを破棄します。"""
        self._sum_cache = None  # 後片付け

c = CacheHolder()
print(c.total)  # 初回は計算
print(c.total)  # 2回目はキャッシュ
del c.total     # キャッシュ破棄
print(c.total)  # 再計算
実行結果
6
6
6

docstringとhelpの書き方

デコレータ形式では、ゲッター関数のdocstringがプロパティのdocstringになります。

help()__doc__で確認できます。

Python
class Meter:
    def __init__(self, value: float) -> None:
        self._value = float(value)

    @property
    def value(self) -> float:
        """メートル(m)単位の長さ。0以上の数値。"""
        return self._value

print(Meter.value.__doc__)  # プロパティのdocstringにアクセス
help(Meter)                 # クラスのヘルプにプロパティ説明が含まれる
実行結果
メートル(m)単位の長さ。0以上の数値。
Help on class Meter in module __main__:

class Meter(builtins.object)
 |  Methods defined here:
 |  
 |  value(self)
 |      メートル(m)単位の長さ。0以上の数値。

実用パターンとサンプル

バリデーションで不正値を防ぐ

ユーザー入力をそのまま属性に入れると危険です。

プロパティで早期に検証し、具体的なメッセージを返すと、使う人が迷いません。

Python
class User:
    def __init__(self, name: str, age: int) -> None:
        self._name = name
        self._age = None
        self.age = age  # セッター経由で検証

    @property
    def age(self) -> int:
        """年齢は0以上の整数。"""
        return self._age

    @age.setter
    def age(self, value: int) -> None:
        if not isinstance(value, int):
            raise TypeError("ageはintで指定してください。例: user.age = 20")
        if value < 0:
            raise ValueError("ageは0以上である必要があります。例: user.age = 0")
        self._age = value

u = User("A", 18)
print(u.age)
# u.age = -1  # ValueError
実行結果
18

計算プロパティで派生データを提供

保存しなくても、元データから計算できる値はプロパティに向いています。

表示用の文字列や合計値などが典型です。

Python
class Rectangle:
    def __init__(self, width: float, height: float) -> None:
        self._width = float(width)
        self._height = float(height)

    @property
    def area(self) -> float:
        """面積(読み取り専用の計算プロパティ)。"""
        return self._width * self._height

r = Rectangle(3, 4)
print(r.area)  # 3×4=12
実行結果
12.0

公開属性を後からプロパティ化して互換維持

最初は単純な公開属性でも、後で検証が必要になることがあります。

同じ名前でプロパティ化すれば、既存コードの記法はそのまま、裏側だけ強化できます。

Python
# 初期版(公開属性)
class ProductV1:
    def __init__(self, price: float) -> None:
        self.price = price  # 直接代入

# 改訂版(プロパティ化して検証追加)
class ProductV2:
    def __init__(self, price: float) -> None:
        self._price = 0.0
        self.price = price  # 既存コードを壊さない

    @property
    def price(self) -> float:
        """価格。0以上の数値。"""
        return self._price

    @price.setter
    def price(self, value: float) -> None:
        if not isinstance(value, (int, float)):
            raise TypeError("priceは数値で指定してください")
        if value < 0:
            raise ValueError("priceは0以上である必要があります")
        self._price = float(value)

p = ProductV2(1200)
print(p.price)
p.price = 1500
print(p.price)
実行結果
1200.0
1500.0

エラーメッセージで使い方を案内

プロパティは使い方を間違えやすい箇所です。

例つきの親切なメッセージにすると、学習コストを下げられます。

Python
class Percentage:
    def __init__(self, value: float) -> None:
        self._value = 0.0
        self.value = value

    @property
    def value(self) -> float:
        """0.0〜1.0の割合。例: 25%は0.25"""
        return self._value

    @value.setter
    def value(self, v: float) -> None:
        if not isinstance(v, (int, float)):
            raise TypeError("valueは数値で指定してください。例: pct.value = 0.25")
        if not (0.0 <= v <= 1.0):
            raise ValueError("valueは0.0〜1.0の範囲で指定してください。例: pct.value = 0.75")
        self._value = float(v)

pct = Percentage(0.3)
print(pct.value)
実行結果
0.3

ベストプラクティスと落とし穴

バッキングフィールドで無限再帰を回避

セッターの中でself.prop = ...と書くと、自分自身のセッターを再帰呼び出ししてしまいます。

必ず_propのような別名のバッキングフィールドに代入します。

Python
class Bad:
    @property
    def x(self) -> int:
        return self._x

    @x.setter
    def x(self, v: int) -> None:
        # self.x = v  # これは無限再帰になるのでNG
        self._x = v  # OK: バッキングフィールドに代入

名前付けと外部APIを安定させる

公開した属性名は外部の利用者のコードに直結します。

将来の変更を見越して外部APIは属性名のままキープし、内部はプロパティ化して進化させると、破壊的変更を避けられます。

バッキングフィールドは先頭にアンダースコアを付け、内部的であることを示しましょう。

パフォーマンスと副作用に注意

プロパティは見た目が軽いので、重いI/Oやネットワーク呼び出しを隠すのは避けるべきです。

計算コストが高い場合は明示的なcompute_*メソッドにする、あるいは結果をキャッシュしたい時はfunctools.cached_property(Python 3.8+)の利用を検討します。

ただしキャッシュの無効化方法も設計しておく必要があります。

Python
from functools import cached_property

class Dataset:
    def __init__(self, path: str) -> None:
        self._path = path

    @cached_property
    def data(self) -> list[str]:
        """初回のみ読み込み、以後はキャッシュしたリストを返す。"""
        # 実際にはファイル読み込みなどを行う想定
        return ["row1", "row2"]

d = Dataset("data.csv")
print(d.data)   # 初回は読み込み
print(d.data)   # 2回目以降はキャッシュ
実行結果
['row1', 'row2']
['row1', 'row2']

型ヒントでgetterとsetterの型をそろえる

型ヒントは可読性とツール支援に有効です。

ゲッターの返り値型とセッターの引数型は一致させます。

Pythonではセッター側のシグネチャに引数型を注釈できます。

Python
class Account:
    def __init__(self, balance: float) -> None:
        self._balance = float(balance)

    @property
    def balance(self) -> float:
        """口座残高。負の値は不可。"""
        return self._balance

    @balance.setter
    def balance(self, value: float) -> None:  # value: float で型を明示
        if value < 0:
            raise ValueError("balanceは0以上である必要があります")
        self._balance = float(value)

継承とオーバーライドの指針

プロパティはディスクリプタとして振る舞うため、サブクラスで上書きできます。

全体を置き換える方法と、.getter/.setterで一部だけ差し替える方法があります。

Python
class Base:
    def __init__(self) -> None:
        self._x = 0

    @property
    def x(self) -> int:
        """基底のx。"""
        return self._x

    @x.setter
    def x(self, v: int) -> None:
        self._x = int(v)

class Sub(Base):
    # 取得だけロギングを追加し、設定は親のまま使う例
    @Base.x.getter
    def x(self) -> int:  # type: ignore[override]
        val = super().x  # 親のゲッターを使う
        print(f"debug: x -> {val}")
        return val

s = Sub()
s.x = 10
print(s.x)  # 取得時にログが出る
実行結果
debug: x -> 10
10
注意

セッターを削除したい場合は、サブクラスで読み取り専用の新たなプロパティを定義して置き換えます。

インターフェースの後方互換性に配慮しましょう。

まとめ

@propertyは、属性アクセスの書き心地とメソッドの柔軟性を両立する強力な仕組みです。

読み取り専用からセッター、デリータ、docstringまで一貫した記法で扱え、後からの仕様強化や互換性維持にも役立ちます。

実装ではバッキングフィールドで無限再帰を避けること、明確なバリデーションと親切なエラーメッセージ過度に重い処理を隠さないといった基本を守ると安心です。

型ヒントや継承の指針も押さえ、プロパティを適材適所で活用してください。

この記事を書いた人
エーテリア編集部
エーテリア編集部

人気のPythonを初めて学ぶ方向けに、文法の基本から小さな自動化まで、実際に手を動かして理解できる記事を書いています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!