紛失届
LOST-010
届出人
計量システムの保守技術者、オ室長
なくしたもの
はかりの記録。重さの検査を付けたのに、-10kg のかばんが記録に載っている。
解決の条件
検査をすり抜けた経路を見つけ、どの経路で入れても検査場を通るようにする。
ハヌルソム国際空港 手荷物センター
オ室長は、マイナスの重さを防ぐために、Bag を作るときに重さを検査するようにしました。ところが記録には -10kg のかばんがあります。ド・ミラン班長が出入りの記録をめくります。「正門に検査場を置いたのに、通用口から入ってきたんだね」
今日は、obj.x を読み書きする瞬間にPythonが通る検査場を見ていきます。7限目で「インスタンス → クラスの順」と言った読み取りの規則の、全体像でもあります。
@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: -10b.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 = …)を守るための仕組みです。
届出人のかばんには、重さ(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
実習台を準備中…
いま変わったもの
封印されたかばん(イミュータブル)
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 Trueb.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 を作ります。継承しても正しい規格で作れる「代替コンストラクタ」に使います。
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__(あれば) | |
| 6 | AttributeError |
データディスクリプタはインスタンスの __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.
クラスにデータディスクリプタ(__get__ + __set__)がある?
インスタンスの__dict__にある?
クラスに非データディスクリプタ(関数のように__get__だけ)がある?
クラスに普通の属性がある?
__getattr__がある?
AttributeError
結果: b. →
今度は、同じ名前をわざとインスタンスの __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.
クラスにデータディスクリプタ(__get__ + __set__)がある?
インスタンスの__dict__にある?
クラスに非データディスクリプタ(関数のように__get__だけ)がある?
クラスに普通の属性がある?
__getattr__がある?
AttributeError
結果: b. →
property もデータディスクリプタです。property はいつも __set__ を持っていて、setter がなければその中で AttributeError を出します。だから __dict__ で隠すことはできません。b.x = v)の順番はもっと短くなります。クラスに __set__ を持つディスクリプタがあればそれを呼び、なければインスタンスの __dict__ に書きます。ピンの案内
→ 順番を1行で覚えるなら「データディスクリプタ > インスタンスの __dict__ > 関数のような非データディスクリプタ > クラス属性 > __getattr__」です。
__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限目)とディスクリプタで十分なので、この講座では名前だけ覚えておいてください。
検査を 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: -10setattr で入れても、新しく作っても、-= で減らしても、すべて止められます。b.kg -= 30 は、b.kg を読み(__get__)、引いた値をまた書く(__set__)という二段階なので、検査場を避けられません。
オ室長がはかりの記録をもう一度流します。マイナスの重さは、もう上がってきません。班長が言います。「扉をふさぐより、すべての道が検査場を通るようにするほうがいい」
解決報告 — LOST-010。重さの検査が init にしかなかったため、その後の b.kg -= 30 が検査をすり抜けていた。検査をデータディスクリプタ(Positive)に移し、すべての書き込みが検査場を通るようにして解決。
Positive を直して、下限値を受け取れるようにしてください(kg = AtLeast(0)、cm = AtLeast(10))。__init__ で受け取った値を検査に使えば大丈夫です。b.weigh = "x" を足すと、b.weigh は何段階目で答えが出るでしょう? b.kg = "x" ではどうでしょう?__getattr__ だけを定義したクラスで、__init__ で入れた属性を読むときに __getattr__ が呼ばれるかどうかを、print で確かめてください。班長の業務手帳
@property で属性の読み書きに検査をかける。検査は setter の一か所に。setter がなければ読み取り専用。
ディスクリプタは __get__/__set__ を持つクラス属性。__set_name__ で付けられた名前を知り、複数の属性で使い回せる。
関数は __get__ だけを持つ非データディスクリプタ。b.m を読むたびに新しいバウンドメソッドができる。
staticmethod は何も束ねず、classmethod はクラスを束ねる。
読む順番:データディスクリプタ > インスタンスの __dict__ > 非データディスクリプタ > クラス属性 > __getattr__。
__getattribute__ はいつも、__getattr__ は見つからなかったときだけ。クラスのクラスは type。
新しい授業・講座のお知らせを受け取る
この講座に授業が追加されたり、続きの講座が公開されたりしたらメールでお知らせします。
R
ReflexKR
触って学ぶPythonとReflex。すべての例はReflex 0.10.0で実際に動かして確認しています。
ReflexKRはバナー広告なし・無料で運営しています。役に立ったら、コーヒー一杯で応援してください。 ☕ コーヒーで応援する
アフィリエイトと支援について© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
質問と回答
この回について気になることを書き込んでください。すべての言語のページの質問がここに集まります。