Python の属性探索順序: データディスクリプタがインスタンスの __dict__ に勝つ理由と __set_name__

obj.x はインスタンスの __dict__ より先にデータディスクリプタを見ます。__set__ か __delete__ の有無で決まる探索順序、cached_property が上書きされる理由、クラス作成後に追加したディスクリプタで __set_name__ が呼ばれない場合を整理しました。

Pythondescriptorattribute-lookupset-name

obj.__dict__["x"] に値を直接入れても、obj.x がその値を返さないことがある。逆に functools.cached_property で作った属性は、インスタンスに同じ名前で代入するとそのまま上書きされる。どちらもディスクリプタなのに結果が違うのは、__set__(または __delete__)を定義しているかどうかの違いだ。

この記事は Python 3.14 のドキュメント(ページ表記は 3.14.8)のデータモデルのリファレンス、Descriptor Guide、functools のページを基準にしていて、いずれも 2026-10-19 に確認した。例の出力は実行して得たものではなく、ドキュメントにある探索規則をコードに沿って一行ずつ当てはめて書いたものだ。ここで説明する探索順序は、既定の object.__getattribute__() を使うインスタンスの場合だ。__getattribute__() を再定義したクラスはこの順序を変えたり迂回したりできるが、object.__getattribute__() に処理を委ねる再定義なら同じ順序を保てる。

ディスクリプタと 2 つの種類

データモデルのリファレンスは、__get__()、__set__()、__delete__() のどれかを定義したオブジェクトをディスクリプタと呼ぶ。__set__() か __delete__() を定義していればデータディスクリプタ、どちらもなければ非データディスクリプタだ。

ディスクリプタはクラス変数として置いたときにだけ働く。Descriptor Guide には、インスタンスに入れたディスクリプタは何の効果もないと書かれている。したがって以下の探索順序は、「インスタンスの __dict__ にある値」と「クラス(と MRO 上の親クラス)にあるディスクリプタ」のどちらが優先されるかという話になる。

インスタンスの探索順序

Descriptor Guide はインスタンスの探索をこうまとめている。データディスクリプタが最優先で、次にインスタンス変数、非データディスクリプタ、クラス変数と続き、最後に __getattr__() が定義されていればそれを呼ぶ。

同じページには object_getattribute() の純 Python 版が載っている。要約すると順番がコードとして見える。

  1. type(obj).__mro__ をたどってクラス側で名前を探す。
  2. 見つかった値の型に __get__ があり、さらに __set__ か __delete__ もあれば(データディスクリプタ)、すぐに __get__(obj, type(obj)) を呼んで結果を返す。インスタンスの __dict__ は見ない。
  3. そうでなければ、インスタンスの __dict__ に名前があるかを見て、あればその値を返す。
  4. クラス側の値が非データディスクリプタなら __get__ を呼ぶ。
  5. ただのクラス変数ならその値を返す。
  6. どれにも当たらなければ AttributeError を送出する。

__getattr__() はこの関数の中にはない。同じページによると、__getattribute__() が AttributeError を送出したときに __getattr__() を呼ぶのはドット演算子と getattr() の役目だ。そのため __getattribute__() を直接呼ぶと __getattr__() は飛ばされる。

データモデルのリファレンスも同じ結論を書いている。__get__() と __set__()(または __delete__())を定義したデータディスクリプタは、インスタンス辞書での再定義を常にオーバーライドし、非データディスクリプタはインスタンス側でオーバーライドできる。通常のメソッドと @staticmethod、@classmethod は非データディスクリプタで、@property はデータディスクリプタだ。インスタンスがメソッドは上書きできても property の振る舞いは上書きできないのはこのためだ。

例

次のファイル 1 つで、この記事の動作をすべて確認できる。Field は __set__ を持つデータディスクリプタで、整数だけを受け付け、値はインスタンスの _名前 属性に保存する。Lazy は __get__ だけを持つ非データディスクリプタで、最初に読まれたときに値を計算し、インスタンスの __dict__ に同じ名前で入れる。functools.cached_property に近い仕組みだが、説明用に単純化したものだ。

class Field:
    def __set_name__(self, owner, name):
        print(f"__set_name__({owner.__name__}, {name!r})")
        self.name = name
        self.storage = "_" + name

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return getattr(obj, self.storage)

    def __set__(self, obj, value):
        if not isinstance(value, int):
            raise TypeError(f"{self.name} must be int")
        setattr(obj, self.storage, value)


class Lazy:
    def __init__(self, func):
        self.func = func

    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        print(f"computing {self.name}")
        value = self.func(obj)
        obj.__dict__[self.name] = value
        return value


print("defining Order")


class Order:
    qty = Field()

    @Lazy
    def label(self):
        return f"order x{self.qty}"

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


print("== 1 lazy")
o = Order(3)
print(o.label)
print(o.label)
print(vars(o))

print("== 2 data descriptor vs __dict__")
o.__dict__["qty"] = "bad"
print(o.qty)
try:
    o.qty = "x"
except TypeError as e:
    print("TypeError:", e)

print("== 3 non-data descriptor vs __dict__")
o.label = "manual"
print(o.label)
del o.label
print(o.label)

print("== 4 added after class creation")
late = Field()
Order.extra = late
try:
    o.extra = 1
except AttributeError as e:
    print("AttributeError:", e)
late.__set_name__(Order, "extra")
o.extra = 1
print(o.extra)

上の規則どおりなら、出力は次のようになる。最後の AttributeError のメッセージの正確な文言は、インタプリタのバージョンによって異なる可能性がある。

defining Order
__set_name__(Order, 'qty')
== 1 lazy
computing label
order x3
order x3
{'_qty': 3, 'label': 'order x3'}
== 2 data descriptor vs __dict__
3
TypeError: qty must be int
== 3 non-data descriptor vs __dict__
manual
computing label
order x3
== 4 added after class creation
AttributeError: 'Field' object has no attribute 'storage'
__set_name__(Order, 'extra')
1

set_name はクラス作成時に一度呼ばれる

__set_name__(Order, 'qty') は、インスタンスを作る前、class Order: 文が実行されるときに出力される。データモデルの __set_name__ の項目によると、このメソッドは所有クラスが作成されるときに自動で呼ばれ、引数はそのクラスと、オブジェクトが代入された名前だ。3.6 で追加された。

クラスオブジェクトの作成の節によると、既定のメタクラス type、または最終的に type.__new__ を呼ぶメタクラスを使う場合、クラスオブジェクトを作ったあとで type.__new__ が名前空間から __set_name__ を持つ属性を集め、それぞれをクラスと名前を渡して呼び、そのあと MRO 上の直接の親の __init_subclass__() を呼ぶ。Descriptor Guide は、__set_name__ はディスクリプタでないオブジェクトでも呼ばれると補足している。

おかげで Field は qty = Field("qty") のように名前を二度書かなくても、保存先の属性名がわかる。

1. 非データディスクリプタは一度だけ計算される

最初に o.label を読むと、インスタンスの __dict__ に label がないので非データディスクリプタの Lazy.__get__ が呼ばれ、computing label が出力される。このメソッドが結果を o.__dict__["label"] に入れたので、2 回目の読み出しは手順 3(インスタンスの __dict__)で値が返る。computing label が一度しか出ないのはそのためだ。

functools.cached_property もドキュメント上は同じ性質を持つ。同じ名前の属性が存在しないときの参照でだけ実行され、実行されると同じ名前の属性に値を書き込み、以後の読み書きは通常の属性と同じように働く。属性を削除すると再計算される。同じページによると、各インスタンスの __dict__ が変更可能なマッピングである必要があるので、__dict__ を含めずに __slots__ を定義したクラスでは動かない。3.12 からは以前あったドキュメント化されていないロックがなくなったため、複数のスレッドから使うと同じインスタンスで getter が 2 回以上実行されることがある。

2. データディスクリプタは dict を無視する

o.__dict__["qty"] = "bad" でインスタンス辞書に qty を直接入れても、o.qty は 3 のままだ。Field はデータディスクリプタなので手順 2 で Field.__get__ が先に呼ばれ、このメソッドは _qty を読む。"bad" は vars(o) には残っているが、o.qty からは見えない。

代入も同じだ。o.qty = "x" はインスタンスの __dict__ には書き込まれず、Field.__set__ を通って TypeError になる。setter のない property が書き込みを拒むのも同じ仕組みだ。Descriptor Guide によると、読み取り専用のデータディスクリプタは __set__() が AttributeError を送出するように定義すればよく、例外を送出するだけの __set__() を定義してもデータディスクリプタになる。

3. 非データディスクリプタはインスタンスが上書きする

Lazy には __set__ がないので、o.label = "manual" はインスタンスの __dict__ にそのまま入り、以後 o.label は manual になる。del o.label でその項目を消すと、次の読み出しでまた Lazy.__get__ が呼ばれて computing label が出る。

関数も同じだ。Descriptor Guide は、関数は束縛メソッドを作るために __get__() を持っており、非データディスクリプタだと説明している。インスタンスにメソッドと同じ名前の属性を代入すると、そのインスタンスでだけメソッドが隠れる。

4. あとから追加したディスクリプタは名前を知らない

Order.extra = late のように、クラスができたあとでディスクリプタを代入すると __set_name__ は自動では呼ばれない。データモデルのリファレンスと Descriptor Guide の両方がこのケースを明記し、必要なら直接呼ぶよう書いている。例では late にまだ storage がないので、o.extra = 1 は AttributeError で終わる。late.__set_name__(Order, "extra") を手で呼んだあとは正常に動く。

Field は 1 が整数かどうかの検査は通り、self.storage を読むところで失敗する。__set_name__ で名前を受け取るディスクリプタを、クラス作成後に setattr() などで追加するコードなら、その場で __set_name__ も呼ぶ必要がある。この一文はドキュメントではなく、上の動作から導いた助言だ。

値をディスクリプタに保存しない

Field が値を self.value のようにディスクリプタ自身に保存すると、ディスクリプタはクラスに 1 つしかないので、すべてのインスタンスが同じ値を見ることになる。だから例では __set_name__ で受け取った名前を使って、値を各インスタンスに置いている。1 つのオブジェクトが複数の場所で共有されて起きる問題という点では、Python のデフォルト引数に [] を書いてはいけない理由と同じ形だ。この節は例の設計についての説明で、共有されるという点はディスクリプタがクラス変数であるというドキュメントの記述から導いたものだ。

参考資料

いずれも 2026-10-19 に確認した。ページ表記は 3.14.8。

ブログへ