R

ReflexKR

Reflex公式ドキュメント
名札の空港:Pythonのオブジェクトモデル
10限目

時間 7:30–8:20

検査場:属性アクセスの秘密

紛失届

LOST-010

重さがマイナスのかばん

届出人

計量システムの保守技術者、オ室長

なくしたもの

はかりの記録。重さの検査を付けたのに、-10kg のかばんが記録に載っている。

解決の条件

検査をすり抜けた経路を見つけ、どの経路で入れても検査場を通るようにする。

ハヌルソム国際空港 手荷物センター

オ室長は、マイナスの重さを防ぐために、Bag を作るときに重さを検査するようにしました。ところが記録には -10kg のかばんがあります。ド・ミラン班長が出入りの記録をめくります。「正門に検査場を置いたのに、通用口から入ってきたんだね」

今日は、obj.x を読み書きする瞬間にPythonが通る検査場を見ていきます。7限目で「インスタンス → クラスの順」と言った読み取りの規則の、全体像でもあります。

10-1. @property:属性に検査場を置く (9分)

届出人のコードです。検査は __init__ にしかありません。

class Bag:
    def __init__(self, kg):
        if kg < 0:
            raise ValueError("negative weight")
        self.kg = kg

b = Bag(20)
b.kg -= 30
print(b.kg)
# -10

作るときは検査を通りましたが、そのあとの b.kg -= 30 は、インスタンスの __dict__ に値を直接書き込みます。通用口です。属性への書き込みそのものに検査をかけるには、@property を使います。

class Bag:
    def __init__(self, kg):
        self.kg = kg
    @property
    def kg(self):
        return self._kg
    @kg.setter
    def kg(self, value):
        if value < 0:
            raise ValueError(f"negative weight: {value}")
        self._kg = value

b = Bag(20)
print(b.kg, vars(b))
# 20 {'_kg': 20}
b.kg -= 30
# ValueError: negative weight: -10
  • b.kg を読むと一つ目の kg 関数が、b.kg = v と書くと @kg.setter の関数が呼ばれます。見た目は普通の属性のように使えます。
  • __init__ の self.kg = kg も setter を通ります。検査は一か所に置くだけで済みます。
  • 実際の値は _kg に入っています。kg という名前は、インスタンスではなくクラスにある property オブジェクトです。

setter を与えなければ、読み取り専用の属性になります。ほかの属性から計算する値に向いています。

class Box:
    def __init__(self, w, h):
        self.w, self.h = w, h
    @property
    def area(self):
        return self.w * self.h

x = Box(3, 4)
print(x.area, type(Box.__dict__["area"]).__name__)
# 12 property
x.area = 5
# AttributeError: property 'area' of 'Box' object has no setter

ピンの案内

→ _kg のようなアンダースコア一つは「触らないでね」という約束にすぎず、鍵ではありません。b._kg = -5 は防げません。検査場は、正規の経路(b.kg = …)を守るための仕組みです。

10-2. ディスクリプタ:検査場を規格書に (10分)

届出人のかばんには、重さ(kg)だけでなく長さ(cm)もあります。属性ごとに property をコピーして付けると、検査のコードが増えていきます。検査場を一つ作って、複数の属性に置くことができます。それがディスクリプタです。

ディスクリプタは、__get__、__set__(または __delete__)のうち一つ以上を持つオブジェクトです。クラス属性として置くと、インスタンスでその名前を読み書きするときに、Pythonがそのメソッドを呼びます。

class Positive:
    def __set_name__(self, owner, name):
        print(f"set_name: {owner.__name__}.{name}")
        self.name = name
    def __get__(self, obj, owner=None):
        if obj is None:
            return self
        return obj.__dict__[self.name]
    def __set__(self, obj, value):
        if value < 0:
            raise ValueError(f"{self.name} must be >= 0: {value}")
        obj.__dict__[self.name] = value

class Bag:
    kg = Positive()
    cm = Positive()
    def __init__(self, kg, cm):
        self.kg, self.cm = kg, cm
# set_name: Bag.kg
# set_name: Bag.cm

b = Bag(20, 55)
print(b.kg, b.cm, vars(b))
# 20 55 {'kg': 20, 'cm': 55}
print(type(Bag.kg).__name__)
# Positive
b.cm = -1
# ValueError: cm must be >= 0: -1
こんなときPythonが呼ぶもの
クラスを作り終えたときPositive.__set_name__(検査場, Bag, "kg")
b.kg を読むときPositive.__get__(検査場, b, Bag)
Bag.kg を読むときPositive.__get__(検査場, None, Bag)
b.kg = 20 と書くときPositive.__set__(検査場, b, 20)
  • __set_name__(3.6)は、class 文が終わった瞬間に、自分が付けられた名前を教えてくれます。だから同じ Positive を kg にも cm にも置けるのです。
  • 検査場オブジェクトはクラスに一つしかなく、すべてのかばんが共有します。そのため値は検査場ではなく、それぞれのかばんの __dict__ に書きます。
  • 値を同じ名前 "kg" で __dict__ に入れたのに、読むときは __dict__ ではなく検査場を通ります。この順番が10-4のテーマです。
  • @property も、Pythonがあらかじめ用意しているディスクリプタです。

タグボード

ステップ 1 / 0

次に実行する行: 0

実習台を準備中…

いま変わったもの

封印されたかばん(イミュータブル)

クラス Bag の箱の kg と cm は、それぞれ name を覚えている Positive の検査場です。最後の行で kg の値はかばんの __dict__ に入り、cm = -1 は検査場で止められます。

10-3. 関数がメソッドになるしくみ (7分)

7限目で、bag.m is bag.m が False になるのを見ました。理由は、関数もディスクリプタだからです。関数は __get__ だけを持ち、__set__ を持たない非データディスクリプタです。

class Bag:
    def weigh(self):
        return 20

b = Bag()
f = Bag.__dict__["weigh"]
print(type(f).__name__, hasattr(f, "__get__"), hasattr(f, "__set__"))
# function True False
m = f.__get__(b, Bag)
print(type(m).__name__, m.__self__ is b, m())
# method True 20
print(b.weigh is b.weigh, b.weigh == b.weigh, Bag.weigh is f)
# False True True

b.weigh を読むたびに、Pythonは f.__get__(b, Bag) を呼んで新しいバウンドメソッドを作ります。関数と b を一緒に束ねた小さなオブジェクトです。だから is は False、== は True です。b.weigh() が self なしで呼べるのも、この束のおかげです。クラスから読むと(Bag.weigh)、束ねるインスタンスがないので、関数がそのまま出てきます。

staticmethod と classmethod は、__get__ が何を束ねるかを変えたディスクリプタです。

class Bag:
    @staticmethod
    def kg_to_lb(kg):
        return round(kg * 2.2046, 1)
    @classmethod
    def empty(cls):
        return cls()

class Suitcase(Bag):
    pass

print(Bag.kg_to_lb(20), Bag().kg_to_lb(20))
# 44.1 44.1
print(type(Suitcase.empty()).__name__, Suitcase.empty.__self__ is Suitcase)
# Suitcase True
print(type(Bag.__dict__["empty"]).__name__)
# classmethod
__get__ が束ねるもの最初の引数
普通の関数インスタンスself
staticmethod何も束ねないなし
classmethodクラス(type(obj))cls

classmethod は呼び出した側のクラスを受け取るので、Suitcase.empty() は Suitcase を作ります。継承しても正しい規格で作れる「代替コンストラクタ」に使います。

10-4. 検査場を通る順番 (9分)

obj.x を読むとき、Python(正確には object.__getattribute__)は次の順に見ていきます。

順番どこを見るか例
1クラス(MRO)のデータディスクリプタ(__set__ か __delete__ があるもの)property、Positive
2インスタンスの __dict__b.label = "ICN"
3クラスの非データディスクリプタ(__get__ だけ)関数、staticmethod、classmethod
4クラスの普通の属性kind = "bag"
5__getattr__(あれば)
6AttributeError

データディスクリプタはインスタンスの __dict__ より前なので、検査場はかばんの中の値より先に呼ばれます。10-2の Positive が、同じ名前で __dict__ に値を入れても問題なかった理由です。逆に、関数は __dict__ より後ろなので、インスタンスに同じ名前を書くとメソッドが隠れます。

下の流れ図で、属性を一つずつ選んでみてください。何段階目で答えが出るかを見せてくれます。

検査場の流れ図

1

class Positive:

2

def __set_name__(self, owner, name):

3

self.name = name

4

def __get__(self, obj, owner):

5

return obj.__dict__[self.name]

6

def __set__(self, obj, value):

7

obj.__dict__[self.name] = value

8

class Bag:

9

kg = Positive()

10

kind = "bag"

11

def weigh(self):

12

return self.kg

13

def __repr__(self):

14

return "Bag()"

15

b = Bag()

16

b.kg = 20

17

b.label = "ICN"

b.

1

クラスにデータディスクリプタ(__get__ + __set__)がある?

2

インスタンスの__dict__にある?

3

クラスに非データディスクリプタ(関数のように__get__だけ)がある?

4

クラスに普通の属性がある?

5

__getattr__がある?

6

AttributeError

結果: b. →

kg は __dict__ にもありますが、1段階目(データディスクリプタ)で先に答えが出ます。label は2段階目、weigh は3段階目(関数)、kind は4段階目、color はどこにもないので AttributeError です。

今度は、同じ名前をわざとインスタンスの __dict__ に入れてみます。最後の行は、検査場を避けようと __dict__ に直接書き込んだ「通用口」です。

検査場の流れ図

1

class Bag:

2

kind = "bag"

3

def weigh(self):

4

return 20

5

@property

6

def tag(self):

7

return "T-" + self.kind

8

def __getattr__(self, name):

9

return f"<no {name}>"

10

b = Bag()

11

b.weigh = "shadowed"

12

b.kind = "suitcase"

13

b.__dict__["tag"] = "fake"

b.

1

クラスにデータディスクリプタ(__get__ + __set__)がある?

2

インスタンスの__dict__にある?

3

クラスに非データディスクリプタ(関数のように__get__だけ)がある?

4

クラスに普通の属性がある?

5

__getattr__がある?

6

AttributeError

結果: b. →

tag は __dict__ に 'fake' があっても property(1段階目)が勝ちます。weigh と kind は __dict__(2段階目)が関数とクラス属性を隠します。color はどこにもないので __getattr__(5段階目)が答えます。
  • setter のない property もデータディスクリプタです。property はいつも __set__ を持っていて、setter がなければその中で AttributeError を出します。だから __dict__ で隠すことはできません。
  • 書き込み(b.x = v)の順番はもっと短くなります。クラスに __set__ を持つディスクリプタがあればそれを呼び、なければインスタンスの __dict__ に書きます。

ピンの案内

→ 順番を1行で覚えるなら「データディスクリプタ > インスタンスの __dict__ > 関数のような非データディスクリプタ > クラス属性 > __getattr__」です。

10-5. __getattr__、__getattribute__、そして type (5分)

名前の似た二つの取っ手は、呼ばれるタイミングが違います。

  • __getattribute__:すべての属性の読み取りでいつも呼ばれます。10-4の順番全体が object.__getattribute__ の中に入っています。自分で上書きすると無限再帰に陥りやすいので、ほとんど使いません。
  • __getattr__:上の順番で見つからなかったときだけ呼ばれる、最後の救助隊です。
class Loud:
    def __init__(self):
        self.kg = 20
    def __getattribute__(self, name):
        print("getattribute", name)
        return super().__getattribute__(name)
    def __getattr__(self, name):
        print("getattr", name)
        return None

x = Loud()
v = x.kg
# getattribute kg
v = x.color
# getattribute color
# getattr color

最後に、クラスもオブジェクトだとしたら、クラスの型は何でしょう? type です。class 文は、実は type(名前, 親たち, 属性の dict) を呼ぶのと同じです。

class Bag:
    kind = "bag"

print(type(Bag()).__name__, type(Bag), type(type))
# Bag <class 'type'> <class 'type'>
Dyn = type("Dyn", (Bag,), {"kind": "dyn"})
print(Dyn.kind, [c.__name__ for c in Dyn.__mro__])
# dyn ['Dyn', 'Bag', 'object']

type を継承して、クラスが作られる過程そのものを変えるものをメタクラスといいます。たいていのことは __init_subclass__(9限目)とディスクリプタで十分なので、この講座では名前だけ覚えておいてください。

事件解決 (10分)

検査を Positive の検査場に移せば、作るときも、書き換えるときも、-= で減らすときも、すべて同じ検査場を通ります。

class Positive:
    def __set_name__(self, owner, name):
        self.name = name
    def __get__(self, obj, owner=None):
        return self if obj is None else obj.__dict__[self.name]
    def __set__(self, obj, value):
        if value < 0:
            raise ValueError(f"{self.name} must be >= 0: {value}")
        obj.__dict__[self.name] = value

class Bag:
    kg = Positive()
    cm = Positive()
    def __init__(self, kg, cm):
        self.kg, self.cm = kg, cm

b = Bag(20, 55)
for change in (lambda: setattr(b, "kg", -1), lambda: Bag(-3, 40)):
    try:
        change()
    except ValueError as e:
        print("blocked:", e)
# blocked: kg must be >= 0: -1
# blocked: kg must be >= 0: -3
b.kg -= 30
# ValueError: kg must be >= 0: -10

setattr で入れても、新しく作っても、-= で減らしても、すべて止められます。b.kg -= 30 は、b.kg を読み(__get__)、引いた値をまた書く(__set__)という二段階なので、検査場を避けられません。

オ室長がはかりの記録をもう一度流します。マイナスの重さは、もう上がってきません。班長が言います。「扉をふさぐより、すべての道が検査場を通るようにするほうがいい」

解決報告 — LOST-010。重さの検査が init にしかなかったため、その後の b.kg -= 30 が検査をすり抜けていた。検査をデータディスクリプタ(Positive)に移し、すべての書き込みが検査場を通るようにして解決。

練習問題

  1. Positive を直して、下限値を受け取れるようにしてください(kg = AtLeast(0)、cm = AtLeast(10))。__init__ で受け取った値を検査に使えば大丈夫です。
  2. 10-4の一つ目の流れ図のコードに b.weigh = "x" を足すと、b.weigh は何段階目で答えが出るでしょう? b.kg = "x" ではどうでしょう?
  3. __getattr__ だけを定義したクラスで、__init__ で入れた属性を読むときに __getattr__ が呼ばれるかどうかを、print で確かめてください。

班長の業務手帳

  • @property で属性の読み書きに検査をかける。検査は setter の一か所に。setter がなければ読み取り専用。

  • ディスクリプタは __get__/__set__ を持つクラス属性。__set_name__ で付けられた名前を知り、複数の属性で使い回せる。

  • 関数は __get__ だけを持つ非データディスクリプタ。b.m を読むたびに新しいバウンドメソッドができる。

  • staticmethod は何も束ねず、classmethod はクラスを束ねる。

  • 読む順番:データディスクリプタ > インスタンスの __dict__ > 非データディスクリプタ > クラス属性 > __getattr__。

  • __getattribute__ はいつも、__getattr__ は見つからなかったときだけ。クラスのクラスは type。

← 前へ

9限目 · 規格書の規格書:継承とMRO

次へ →

11限目 · 持ち主のいないかばん:メモリとGC

新しい授業・講座のお知らせを受け取る

この講座に授業が追加されたり、続きの講座が公開されたりしたらメールでお知らせします。

収集項目:メールアドレス・目的:この講座の新しい授業・公開のお知らせ・保有期間:解除のご依頼まで。他の目的には使わず、第三者にも提供しません。お知らせメールに返信いただければすぐに削除します。

質問と回答

この回について気になることを書き込んでください。すべての言語のページの質問がここに集まります。

リンクや書式なしで、そのままの文字で表示されます。

R

ReflexKR

触って学ぶPythonとReflex。すべての例はReflex 0.10.0で実際に動かして確認しています。

ReflexKRはバナー広告なし・無料で運営しています。役に立ったら、コーヒー一杯で応援してください。 ☕ コーヒーで応援する

アフィリエイトと支援について
한국어English日本語简体中文EspañolDeutsch

© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0

Built with Reflex