Pythonのproperty(プロパティ)とは、メソッドを「属性のように」読み書きできるようにする仕組みです。 obj.price と書くだけで裏側の関数が呼ばれるので、呼び出す側の書き方を変えずに、値のチェックや計算をあとから差し込めます。getter(読み出し)だけを定義すれば、代入できない読み取り専用の属性になります。

この記事では、@property・@x.setter・@x.deleter の書き方を、そのまま貼って動くコードと実行結果でそろえました。読み取り専用属性へ代入したときの AttributeError を Python 3.10.18/3.11.15/3.12.3/3.13.7/3.14.7 の5つで実際に動かして比べた結果、self.x = と書いて起きる RecursionError、継承で setter が消える現象と直し方、functools.cached_property・dataclass(frozen=True)・__slots__ との使い分けまで、1ページで確認できます。

propertyの早見表

まず、書き方と「何が起きるか」を1枚にまとめます。迷ったらこの表の左列から、やりたいことを探してください。

やりたいこと 書き方 呼ばれるタイミング 定義しないとき
値を読み出す(getter) @property を付けた def x(self) obj.x (必須。これが土台)
値を代入する(setter) @x.setter を付けた def x(self, value) obj.x = 値 代入すると AttributeError
値を削除する(deleter) @x.deleter を付けた def x(self) del obj.x 削除すると AttributeError
読み取り専用にする getter だけを書く obj.x のみ ―
代入時に値をチェックする setter の中で raise ValueError(...) obj.x = 値、__init__ 内の self.x = 値 ―
計算結果を属性に見せる getter の中で計算して return obj.x のたびに計算 ―
重い計算を1回だけにする @functools.cached_property 初回の obj.x だけ計算 ―
デコレータを使わずに書く x = property(fget, fset, fdel, doc) 上と同じ 省いた引数の操作は AttributeError

よく出るエラーと原因も先に並べておきます(メッセージはすべて今回の実行結果です)。

エラーメッセージ 原因 直し方
AttributeError: property 'radius' of 'Circle' object has no setter(3.11以降) setter が無いプロパティへ代入した 代入させたいなら @radius.setter を追加
AttributeError: can't set attribute 'radius'(3.10) 同上(3.10でのメッセージ) 同上
RecursionError: maximum recursion depth exceeded getter/setter の中で self.price と自分自身を呼んだ 実体は self._price のように別名で持つ
TypeError: 'int' object is not callable プロパティに () を付けて obj.price() と呼んだ かっこを外して obj.price
TypeError: No '__dict__' attribute on 'Slim' instance to cache 'total' property. __slots__ のクラスで cached_property を使った __slots__ に "__dict__" を含めるか @property にする

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Python propertyの使い方|setterと読み取り専用属性」

確認した環境とこの記事の読み方

実行結果は次の環境で取りました。とくに断りのない出力は Python 3.11.15 のものです。

項目 内容
OS Linux(コンテナ)
主に使った版 Python 3.11.15
版の違いを比べた版 Python 3.10.18/3.12.3/3.13.7/3.14.7
使ったもの 標準ライブラリのみ(functools、dataclasses)

コードは1ファイルに貼って python3 ファイル名.py で動きます。トレースバック内のファイルパスは、読みやすいように短い名前へ置き換えています。

propertyとは何か、なぜ使うのか

Pythonでは、クラスの属性は基本的にそのまま読み書きできます。

class Item:
    def __init__(self, name, price):
        self.name = name
        self.price = price

item = Item("ノート", 300)
item.price = -50      # 何でも代入できる

最初はこれで十分です。ところが、あとから「単価にマイナスは入れたくない」「税込み価格も属性として見せたい」となったときに、get_price() や set_price() というメソッドへ切り替えると、呼び出している側のコードをすべて書き直すことになります。

propertyを使うと、呼び出し側は item.price のまま、クラスの中だけを変えられます。

比べる点 ふつうの属性 get_/set_ メソッド property
呼び出し側の書き方 item.price item.get_price() item.price
代入時のチェック 入れられない 入れられる 入れられる
読み取り専用 できない setを作らなければ可 setterを書かなければ可
あとから追加したとき ― 呼び出し側を全部直す 呼び出し側はそのまま

「最初はふつうの属性で始め、チェックや計算が必要になった時点でpropertyに替える」のがPythonらしい進め方です。最初から全部の属性をpropertyにする必要はありません。

基本の書き方:getterとsetterで値をチェックする

もっともよく使う形です。実体は _price のように下線を付けた別名で持ち、price という名前のプロパティを窓口にします。

class Item:
    def __init__(self, name, price):
        self.name = name
        self.price = price      # ここでも setter が呼ばれる

    @property
    def price(self):
        """税抜きの単価(0以上の整数)"""
        return self._price

    @price.setter
    def price(self, value):
        if not isinstance(value, int):
            raise TypeError(f"price は int で指定します: {value!r}")
        if value < 0:
            raise ValueError(f"price は 0 以上にします: {value}")
        self._price = value

    @property
    def price_with_tax(self):
        return int(self._price * 1.1)


item = Item("ノート", 300)
print(item.price)            # getter
item.price = 450             # setter
print(item.price)
print(item.price_with_tax)   # 計算して返す読み取り専用

for bad in (-1, "500"):
    try:
        item.price = bad
    except (ValueError, TypeError) as e:
        print(type(e).__name__, ":", e)

try:
    Item("鉛筆", -10)
except ValueError as e:
    print("生成時も検証:", e)
print(item.__dict__)

実行結果:

300
450
495
ValueError : price は 0 以上にします: -1
TypeError : price は int で指定します: '500'
生成時も検証: price は 0 以上にします: -10
{'name': 'ノート', '_price': 450}

押さえておきたい点は4つです。

ポイント 説明
書く順番 先に @property で getter、そのあとに @price.setter。setter のデコレータ名は プロパティ名.setter
関数名 getter も setter も 同じ名前(ここでは price)にする
実体の名前 self._price のように別名にする(同じ名前にすると後述の RecursionError)
__init__ での代入 self.price = price と書けば、生成時にも setter のチェックが通る

最後の item.__dict__ を見ると、インスタンスが実際に持っているのは _price だけで、price はクラス側に置かれた窓口だとわかります。

値の種類ごとに使う例外

setter でチェックに通らなかったときは、内容に合った例外を送出します。

状況 送出する例外 例
型が違う TypeError 整数のはずが文字列
型は合うが値が範囲外 ValueError マイナス、空文字、上限超え
その操作自体を受け付けない AttributeError 読み取り専用、破棄済み

メッセージには受け取った値そのもの({value!r})を入れておくと、あとで調べるときに手がかりになります。

読み取り専用属性を作る:setterを書かない

getter だけを定義すると、読み取り専用の属性になります。代入や削除をすると AttributeError です。

import sys

class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):
        return self._radius

    @property
    def area(self):
        return 3.14159 * self._radius ** 2


c = Circle(2)
print(sys.version.split()[0])
print(c.radius, c.area)
try:
    c.radius = 5
except AttributeError as e:
    print("AttributeError:", e)
try:
    del c.radius
except AttributeError as e:
    print("AttributeError:", e)

Python 3.10以前と3.11以降でメッセージが違う

同じコードを5つの版で動かした結果です。3.11からメッセージが「どのクラスのどのプロパティに setter が無いか」を伝える文に変わっています。

Pythonの版 代入したときのメッセージ 削除したときのメッセージ
3.10.18 can't set attribute 'radius' can't delete attribute 'radius'
3.11.15 property 'radius' of 'Circle' object has no setter property 'radius' of 'Circle' object has no deleter
3.12.3 property 'radius' of 'Circle' object has no setter property 'radius' of 'Circle' object has no deleter
3.13.7 property 'radius' of 'Circle' object has no setter property 'radius' of 'Circle' object has no deleter
3.14.7 property 'radius' of 'Circle' object has no setter property 'radius' of 'Circle' object has no deleter

3.9以前は今回動かしていません(未実行)。検索でよく見かける AttributeError: can't set attribute という文は、こうした以前の版で出るメッセージです。

try を外してそのまま実行したときのトレースバックも比べておきます。

Python 3.10.18:

Traceback (most recent call last):
  File "circle.py", line 10, in <module>
    c.radius = 5
AttributeError: can't set attribute 'radius'

Python 3.11.15/3.14.7:

Traceback (most recent call last):
  File "circle.py", line 10, in <module>
    c.radius = 5
    ^^^^^^^^
AttributeError: property 'radius' of 'Circle' object has no setter

どちらのメッセージでも、意味は「そのプロパティには setter が定義されていない」です。 代入させたいなら setter を足し、読み取り専用のままでよいなら、代入している側のコードを見直します。

「読み取り専用」はどこまで守られるか

propertyの読み取り専用は、うっかり代入に気づけるようにする仕組みです。下線付きの実体へは代入できますし、つづりを間違えた名前は新しい属性として作られます。

class Plain:
    def __init__(self):
        self._radius = 1

    @property
    def radius(self):
        return self._radius

q = Plain()
q._radius = 99                    # 下線付きの実体には代入できる
q.raduis = 5                      # つづり違いは新しい属性になる
print(q.radius, q.__dict__)
99 {'_radius': 99, 'raduis': 5}

「下線で始まる名前は外から触らない」という約束ごとと組み合わせて使うもの、と理解しておくとすっきりします。つづり違いまで止めたいときは後述の __slots__、インスタンス全体を変更不可にしたいときは dataclass(frozen=True) が向いています。

self.x = と書いて起きるRecursionErrorと直し方

propertyで最初につまずきやすいのが、実体の名前とプロパティの名前を同じにしてしまう書き方です。

エラーになる例:

class Item:
    def __init__(self, price):
        self.price = price

    @property
    def price(self):
        return self.price        # 自分自身(プロパティ)をまた呼ぶ

    @price.setter
    def price(self, value):
        self.price = value       # 自分自身(setter)をまた呼ぶ

try:
    Item(100)
except RecursionError as e:
    print("RecursionError:", e)
RecursionError: maximum recursion depth exceeded

self.price = value は「price の setter を呼ぶ」という意味です。その setter の中でまた self.price = value と書いているので、setter が自分を呼び続け、呼び出しの深さが上限に達して止まります。try を外して実行すると、同じ行がくり返されている様子がトレースバックに出ます(Python 3.11.15)。

Traceback (most recent call last):
  File "item.py", line 13, in <module>
    Item(100)
  File "item.py", line 3, in __init__
    self.price = price
    ^^^^^^^^^^
  File "item.py", line 11, in price
    self.price = value       # 自分自身(setter)をまた呼ぶ
    ^^^^^^^^^^
  [Previous line repeated 994 more times]
RecursionError: maximum recursion depth exceeded

直した例(実体を _price にする):

class Item:
    def __init__(self, price):
        self.price = price       # ここは setter 経由でよい

    @property
    def price(self):
        return self._price       # 実体は別名

    @price.setter
    def price(self, value):
        self._price = value      # 実体は別名
場所 書く名前 理由
getter・setter の中 self._price 実体に直接触れる。ここで self.price と書くと自分を呼び続ける
__init__ やほかのメソッド self.price setter のチェックを通したいので窓口経由にする
クラスの外 obj.price 窓口経由

「[Previous line repeated ... more times] と出たら、getter か setter の中の名前に下線が付いているかを見る」と覚えておくと、すぐに直せます。

deleterでdel文の動きを決める

@x.deleter を定義すると、del obj.x と書いたときの動きを決められます。使う場面は多くありませんが、キャッシュや一時的な値を捨てる操作を表すのに向いています。

class Session:
    def __init__(self, token):
        self._token = token

    @property
    def token(self):
        if self._token is None:
            raise AttributeError("token は破棄済みです")
        return self._token

    @token.setter
    def token(self, value):
        self._token = value

    @token.deleter
    def token(self):
        print("token を破棄します")
        self._token = None


s = Session("abc123")
print(s.token)
del s.token
try:
    s.token
except AttributeError as e:
    print("AttributeError:", e)
print(hasattr(s, "token"))
s.token = "xyz789"
print(s.token)
abc123
token を破棄します
AttributeError: token は破棄済みです
False
xyz789

getter の中で AttributeError を送出すると、hasattr(s, "token") は False を返します。hasattr は「読み出してみて AttributeError になるか」で判定しているためです。「値がまだ無い」ことを属性が無い状態として見せたいときに使えます。

なお、getter の中で別の属性のつづりを間違えた場合も AttributeError になり、メッセージには間違えたほうの名前が出ます。

class Hidden:
    @property
    def size(self):
        return self._sizee      # つづり違い
try:
    Hidden().size
except AttributeError as e:
    print("AttributeError:", e)
AttributeError: 'Hidden' object has no attribute '_sizee'

メッセージに出ている名前(_sizee)を getter の中で探すと、原因の行が見つかります。

property()関数で書く方法

@property はデコレータとして使うのが一般的ですが、もとは property() という組み込みの呼び出しです。引数は property(fget=None, fset=None, fdel=None, doc=None) の4つです。

class Member:
    def __init__(self, name):
        self._name = name

    def get_name(self):
        return self._name

    def set_name(self, value):
        if not value:
            raise ValueError("name は空にできません")
        self._name = value

    def del_name(self):
        del self._name

    name = property(get_name, set_name, del_name, "会員の表示名")


m = Member("佐藤")
print(m.name)
m.name = "鈴木"
print(m.name)
print(Member.name.__doc__)
print(Member.name.fget, Member.name.fset is Member.set_name)
try:
    m.name = ""
except ValueError as e:
    print("ValueError:", e)
佐藤
鈴木
会員の表示名
<function Member.get_name at 0x7f33ba719800> True
ValueError: name は空にできません

(0x7f33ba719800 の部分は実行のたびに変わります。)

比べる点 @property デコレータ property() 関数
書きやすさ getter・setter が同じ名前でまとまる get_/set_ のメソッドが別に残る
向いている場面 新しく書くクラス すでにある get_x/set_x を属性としても見せたいとき
説明文(docstring) getter の docstring が使われる 第4引数で渡せる
元の関数の取り出し クラス.x.fget/.fset/.fdel 同じ

@property と書くのは、price = property(price) と書くのと同じ意味です。デコレータ版は「関数版を短く書くための書き方」 だとわかると、@price.setter が「いまある price プロパティに setter を足した新しいプロパティを作る」動きだということも理解しやすくなります。

Python公式ドキュメントの組み込み関数のページ(Python 3.14版)では、3.5でプロパティの docstring が書き換えられるようになったこと、3.13でプロパティが __name__ 属性を持つようになったことが記されています。手元でも、3.14.7では C.x.__name__ が x を返し、3.11.15では __name__ 属性が無いことを確かめました。

継承したクラスでプロパティを上書きする

親クラスのプロパティを子クラスで変えるときは、少し注意が要ります。プロパティは getter・setter・deleter をひとまとめにした1個のオブジェクトなので、子クラスで @property と書き直すと、親の setter は引き継がれません。

class Product:
    def __init__(self, price):
        self.price = price

    @property
    def price(self):
        return self._price

    @price.setter
    def price(self, value):
        if value < 0:
            raise ValueError("price は 0 以上にします")
        self._price = value


# エラーになる例: getter だけ書き直すと setter が消える
class SaleA(Product):
    @property
    def price(self):
        return int(self._price * 0.8)

try:
    SaleA(1000)
except AttributeError as e:
    print("SaleA  AttributeError:", e)


# 直した例1: 親のプロパティから getter だけ差し替える
class SaleB(Product):
    @Product.price.getter
    def price(self):
        return int(self._price * 0.8)

b = SaleB(1000)
print("SaleB ", b.price)
b.price = 2000
print("SaleB ", b.price)


# 直した例2: setter に検証を足す(親の setter を呼ぶ)
class SaleC(Product):
    @Product.price.setter
    def price(self, value):
        if value % 10:
            raise ValueError("price は 10 円単位にします")
        Product.price.fset(self, value)

c = SaleC(1000)
try:
    c.price = 1234
except ValueError as e:
    print("SaleC  ValueError:", e)
try:
    c.price = -10
except ValueError as e:
    print("SaleC  ValueError:", e)
print("SaleC ", c.price)
print(Product.price is SaleB.price, type(SaleB.price).__name__)


# 直した例3: プロパティは固定し、中で呼ぶメソッドを子で差し替える
class Base:
    def __init__(self, price):
        self._price = price

    @property
    def price(self):
        return self._get_price()

    def _get_price(self):
        return self._price

class Half(Base):
    def _get_price(self):
        return self._price // 2

print("Half  ", Half(1000).price)
SaleA  AttributeError: property 'price' of 'SaleA' object has no setter
SaleB  800
SaleB  1600
SaleC  ValueError: price は 10 円単位にします
SaleC  ValueError: price は 0 以上にします
SaleC  1000
False property
Half   500

SaleA は、親の __init__ が self.price = price と代入した時点で止まります(Python 3.10.18では can't set attribute 'price' と表示されました)。子クラスの price は getter だけの新しいプロパティに置き換わっているためです。

やりたいこと 書き方 特徴
getter だけ変える @親クラス.x.getter 親の setter・deleter を残したまま新しいプロパティを作る
setter だけ変える @親クラス.x.setter 親の getter を残す。親の検証も使うなら 親クラス.x.fset(self, value) を呼ぶ
全部作り直す 子で @property と @x.setter を両方書く 親とは別物になる
子で差し替える前提で設計する getter から self._get_x() を呼ぶ 子はふつうのメソッドを上書きするだけで済む

Product.price is SaleB.price が False になっているとおり、.getter や .setter は親のプロパティを書き換えるのではなく、コピーして新しいプロパティを返します。親クラスの動きは変わりません。

子クラスで何度も差し替えることがわかっているなら、3つめの「メソッドを呼ぶだけのプロパティ」にしておくと、子クラス側がふつうのメソッド上書きだけで済み、読みやすくなります。

cached_propertyで重い計算を1回だけにする

@property の getter は、読み出すたびに実行されます。集計のように時間がかかる処理を毎回動かしたくないときは、標準ライブラリの functools.cached_property(Python 3.8で追加)が使えます。

import time
from functools import cached_property


class Report:
    def __init__(self, rows):
        self.rows = rows

    @property
    def total_slow(self):
        print("  (毎回計算)")
        return sum(self.rows)

    @cached_property
    def total(self):
        print("  (初回だけ計算)")
        time.sleep(0.5)
        return sum(self.rows)


r = Report([10, 20, 30])
print(r.total_slow); print(r.total_slow)

t = time.perf_counter(); print(r.total); print(f"1回目 {time.perf_counter() - t:.3f} 秒")
t = time.perf_counter(); print(r.total); print(f"2回目 {time.perf_counter() - t:.3f} 秒")

print(r.__dict__)
r.rows.append(40)
print("rows を変えても:", r.total)
del r.total                       # キャッシュを捨てる
print("del 後:", r.total)
r.total = 999                     # 代入もできる(読み取り専用ではない)
print("代入後:", r.total)
  (毎回計算)
60
  (毎回計算)
60
  (初回だけ計算)
60
1回目 0.500 秒
60
2回目 0.000 秒
{'rows': [10, 20, 30], 'total': 60}
rows を変えても: 60
  (初回だけ計算)
del 後: 100
代入後: 999

cached_property は、初回の結果をインスタンスの __dict__ に同じ名前で保存することで、2回目以降の計算を省いています。r.__dict__ に 'total': 60 が入っているのがその証拠です。この仕組みから、次の性質が出てきます。

性質 内容 対応
元データを変えても値は変わらない rows に追加しても 60 のまま del obj.total でキャッシュを捨てると次の読み出しで再計算
代入できる r.total = 999 が通る 読み取り専用にしたいなら @property を使う
__dict__ が必要 __slots__ だけのクラスでは使えない __slots__ に "__dict__" を加える、または @property
複数スレッドでは getter が複数回動くことがある 3.12でロックが外された 必要なら getter の中で自分でロックを取る

__slots__ のクラスで使ったときのエラーも確認しました(3.10.18/3.11.15/3.14.7 で同じ文)。

from functools import cached_property

class Slim:
    __slots__ = ("rows",)

    def __init__(self, rows):
        self.rows = rows

    @cached_property
    def total(self):
        return sum(self.rows)

try:
    Slim([1, 2]).total
except TypeError as e:
    print("TypeError:", e)
TypeError: No '__dict__' attribute on 'Slim' instance to cache 'total' property.

スレッドについては、公式ドキュメントの functools のページに、3.12より前は文書化されていないロックが入っていたこと、3.12以降はそのロックが取り除かれ、同じインスタンスで getter が複数回動く可能性があることが書かれています(今回このページを開いて確認しました。複数スレッドでの動作そのものは未実行です)。

「あとから変わらない値を、一度だけ計算したい」ときが cached_property の出番です。元データが変わるたびに値も変わってほしいなら、ふつうの @property が合っています。

変更できないオブジェクトを作るほかの方法

読み取り専用にする方法はpropertyだけではありません。目的が「1つの属性を守る」なのか「インスタンス全体を固定する」なのかで選びます。

from dataclasses import dataclass, FrozenInstanceError


@dataclass(frozen=True)
class Point:
    x: int
    y: int

    @property
    def norm2(self):
        return self.x ** 2 + self.y ** 2


p = Point(3, 4)
print(p, p.norm2)
try:
    p.x = 10
except FrozenInstanceError as e:
    print("FrozenInstanceError:", e)
print(issubclass(FrozenInstanceError, AttributeError))
object.__setattr__(p, "x", 10)   # 抜け道はある(通常は使わない)
print(p)


class Slotted:
    __slots__ = ("x", "y")

    def __init__(self, x, y):
        self.x = x
        self.y = y

s = Slotted(1, 2)
s.x = 100                         # 代入はできる
try:
    s.z = 3                       # 宣言していない名前は足せない
except AttributeError as e:
    print("AttributeError:", e)

Python 3.11.15:

Point(x=3, y=4) 25
FrozenInstanceError: cannot assign to field 'x'
True
Point(x=10, y=4)
AttributeError: 'Slotted' object has no attribute 'z'

Python 3.14.7では最後の行が次のように、理由まで添えた文になっていました。

AttributeError: 'Slotted' object has no attribute 'z' and no __dict__ for setting new attributes

わかることは3つです。

  • dataclass(frozen=True) はすべてのフィールドへの代入を止める。例外は FrozenInstanceError で、AttributeError の子クラスなので except AttributeError でも受け取れる
  • frozen な dataclass の中でも @property は使える(計算で求める値を足すのに便利)
  • __slots__ は宣言していない名前の追加を止める。宣言した属性への代入は止めない

使い分け早見表:property・cached_property・frozen・slots

ここまでの内容を、選ぶための表にまとめます。

方法 代入を止める範囲 代入時のチェック 計算のタイミング 向いている場面
ふつうの属性 止めない できない ― まずはこれ。チェックも計算も要らない値
@property(getterのみ) その属性だけ ― 読むたび 読み取り専用にしたい値、ほかの属性から計算する値
@property と setter 止めない(検証する) できる 読むたび 範囲や型を守りたい値
property() 関数 引数しだい できる 読むたび 既存の get_/set_ メソッドを属性としても見せる
functools.cached_property 止めない できない 初回だけ 重い計算で、あとから変わらない値
dataclass(frozen=True) 全フィールド __post_init__ で生成時のみ ― 座標や設定値など、作ったら変えないデータ
__slots__ 宣言外の名前の追加 できない ― つづり違いに気づきたい、インスタンスを軽くしたい

選び方を順番にたどると次のようになります。

flowchart TD
    A[属性を定義したい] --> B{代入時に値を確かめたい?}
    B -- はい --> C[property と setter]
    B -- いいえ --> D{ほかの属性から計算する値?}
    D -- はい --> E{計算が重く 値は変わらない?}
    E -- はい --> F[cached_property]
    E -- いいえ --> G[property getterのみ]
    D -- いいえ --> H{インスタンス全体を固定したい?}
    H -- はい --> I[dataclass frozen=True]
    H -- いいえ --> J{1つだけ読み取り専用にしたい?}
    J -- はい --> G
    J -- いいえ --> K[ふつうの属性]

組み合わせも可能です。たとえば「frozen な dataclass に @property で計算値を足す」「@property で検証しつつ __slots__ でつづり違いも防ぐ(__slots__ = ("_price",) と実体の名前を宣言する)」といった形です。

つまずきやすい点と確認リスト

そのほか、今回動かして確かめた細かい挙動をまとめます。

class Item:
    def __init__(self, price):
        self._price = price

    @property
    def price(self):
        return self._price

i = Item(100)
try:
    i.price()
except TypeError as e:
    print("TypeError:", e)
print(type(Item.price))
i.__dict__["price"] = 5
print(i.price)


class NoSetterOrder:
    @property
    def v(self): return 1
    @v.setter
    def w(self, value): pass     # 名前が v ではなく w

n = NoSetterOrder()
try:
    n.v = 3
except AttributeError as e:
    print("AttributeError:", e)
n.w = 3
print(n.w)
TypeError: 'int' object is not callable
<class 'property'>
100
AttributeError: property 'v' of 'NoSetterOrder' object has no setter
1
現象 理由 対応
obj.price() で 'int' object is not callable obj.price の時点で値(整数)が返り、それに () を付けている かっこを外す
クラス.price は値ではなく property オブジェクト インスタンス経由のときだけ getter が動く 値が欲しいときはインスタンスから読む
__dict__ に同じ名前を直接入れてもプロパティが優先される プロパティはインスタンスの辞書より先に調べられる 実体は別名(_price)で持つ
setter を書いたのに has no setter と出る setter の関数名が getter と違う(上の例では w) getter と setter は同じ関数名にする
getter に引数を渡したい プロパティは引数を取れない 引数が要るならふつうのメソッドにする

最後に、書き終えたら見直したい項目です。

確認すること 見るところ
実体の名前に下線が付いているか getter・setter の中が self._x になっている
getter・setter・deleter の関数名が同じか 3つとも def x
__init__ でも検証が通るか self.x = 値(窓口経由)で代入している
getter が軽いか 読むたびに動くので、重い処理は cached_property かメソッドへ
getter が状態を変えていないか 読むだけで値が変わると追いにくい。変える操作はメソッドにする
継承先で setter が残っているか 子では @親.x.getter/@親.x.setter を使う

propertyとメソッドのどちらにするか

「属性のように見せる」か「メソッドとして呼ばせる」かで迷ったときの目安です。

判断の観点 property が合う メソッドが合う
引数 要らない 要る
処理の重さ すぐ終わる 時間がかかる(通信、ファイル、大量データ)
呼ぶたびの結果 同じ状態なら同じ値 呼ぶたびに変わることがある
副作用 無い ある(保存する、送る、状態を変える)
読む人の受け取り方 「値を見ているだけ」 「何か処理をしている」

obj.total と書かれていたら、読む人は「すぐ返ってくる値」だと受け取ります。その期待に合う処理だけをpropertyにすると、使う側にとって素直なクラスになります。

よくある質問

Q. getterだけ欲しいとき、@propertyを付ける意味はありますか?

A. あります。@property を付けると obj.area のようにかっこ無しで読め、代入しようとすると AttributeError で知らせてくれます。ほかの属性から計算で求める値(面積、合計、フルネームなど)を属性として見せたいときに向いています。

Q. 「AttributeError: can't set attribute」と出ました。どう直せばよいですか?

A. setter が無いプロパティへ代入しています。Python 3.10では can't set attribute '名前'、3.11以降では property '名前' of 'クラス名' object has no setter と表示されることを今回の実行で確かめました。代入させたいなら @名前.setter を追加し、読み取り専用のままにしたいなら代入している行を見直します。継承したクラスで @property を書き直した場合も同じメッセージになります。

Q. setterの中で「self.x = value」と書いたらRecursionErrorになりました。

A. self.x = value は setter 自身をもう一度呼ぶ書き方なので、呼び出しが終わらなくなります。実体は self._x のように別の名前で持ち、getter と setter の中ではその別名を読み書きしてください。

Q. propertyとcached_propertyはどう使い分けますか?

A. 読むたびに最新の値を返したいなら @property、計算が重くて一度求めれば変わらない値なら functools.cached_property です。cached_property は代入もでき、元データを変えても値は自動では更新されません。更新したいときは del obj.名前 でキャッシュを捨てます。

まとめ

  • @property は「メソッドを属性として見せる」仕組み。呼び出し側を obj.x のまま、チェックや計算をあとから差し込める
  • getter だけ書けば読み取り専用。代入時のメッセージは3.10で can't set attribute 'x'、3.11以降で property 'x' of 'クラス名' object has no setter
  • setter では TypeError/ValueError で値をチェックし、__init__ でも self.x = 値 と窓口経由で代入する
  • 実体は self._x と別名にする。getter・setter の中で self.x と書くと RecursionError
  • 継承先では @親クラス.x.getter/@親クラス.x.setter を使うと、残したいほうを引き継げる
  • 重い計算は cached_property、全体を固定するなら dataclass(frozen=True)、名前の追加を止めるなら __slots__ と使い分ける
  • 引数が要る・時間がかかる・状態を変える処理はメソッドにして、propertyは「すぐ返る値」に絞る

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Python propertyの使い方|setterと読み取り専用属性」

株式会社RJCでは、Pythonのクラス設計やpropertyの使い方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?