- Python
Python propertyの使い方|setterと読み取り専用属性
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期連続の黒字決算。