R

ReflexKR

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

時間 5:00–5:50

かばんの規格書:クラスとインスタンス

紛失届

LOST-007

一枚貼っただけなのに、全部に貼られたステッカー

届出人

オーケストラ巡回公演の輸送担当、ムン室長

なくしたもの

楽器ケースの一つに「割れ物注意」のステッカーを貼ったのに、同じ規格で作ったケースすべてにそのステッカーが貼られている。

解決の条件

ステッカーのリストが実際にどこにあるのかを見つけ、ケースごとに自分のステッカーリストを持たせる。

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

ド・ミラン班長の机の引き出しには、かばんの規格書がひと束入っています。規格書が一枚あれば、同じ形のかばんをいくつでも作れます。「規格書も一枚の紙、かばんも一つひとつ。どっちも物なんだ」

ムン室長は、バイオリンケースに「割れ物注意」を貼っただけなのに、チェロケースにも同じステッカーが見えるといいます。班長はケースではなく規格書を広げてみます。ステッカーはそこに貼られていました。

7-1. 規格書もかばんである:クラスオブジェクト (7分)

class 文は、クラスオブジェクトを作って名前に結び付ける代入です(1限目の def と同じです)。クラスも型を持つオブジェクトで、その型は type です。

class Case:
    size = "M"
    def __init__(self, owner):
        self.owner = owner

print(type(Case), isinstance(Case, type))
# <class 'type'> True
b = Case("cello")
print(type(b).__name__, type(b) is Case)
# Case True
print(vars(b))
# {'owner': 'cello'}
print([k for k in vars(Case) if not k.startswith("__")])
# ['size']
print(b.size, "size" in vars(b))
# M False
何を入れているか見る方法
クラス(規格書)クラス本体で作った名前:クラス属性、メソッドvars(Case)、Case.__dict__
インスタンス(かばん)そのかばんだけの属性。たいていは __init__ で self.x = ... としてvars(b)、b.__dict__
  • b の __dict__ には owner しかありません。それなのに b.size は "M" と読めます。
  • 属性を読むときは、まずインスタンスの __dict__ を見て、なければクラスを見ます。かばんになければ規格書を広げてみる、というわけです。
  • クラスの __dict__ は mappingproxy という読み取り専用の窓です。クラス属性を変えたいときは、Case.size = "L" のようにクラスに直接代入します。

7-2. 皆で共有するクラス属性 (10分)

届出人の規格書です。

class Case:
    stickers = []
    def __init__(self, owner):
        self.owner = owner

a = Case("violin")
b = Case("cello")
a.stickers.append("FRAGILE")
print(b.stickers, Case.stickers)
# ['FRAGILE'] ['FRAGILE']
print(a.stickers is b.stickers, "stickers" in vars(a))
# True False
  • stickers = [] はクラス本体で一度だけ実行されます。リストは規格書に一つ貼られているだけです。
  • a.stickers は a の __dict__ にないのでクラスで見つかり、append はその一つしかないリストを変えます(かばんの変更)。
  • 4限目のデフォルト引数と同じ形のバグです。「一度作ったミュータブルなオブジェクトを、皆で共有している」

インスタンスごとに別々に持つべきものは、__init__ で self に付けます。下で三つのやり方を比べてみてください。

タグボード

ステップ 1 / 0

次に実行する行: 0

実習台を準備中…

いま変わったもの

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

一つ目のやり方では、リストは class Case の箱にしかなく、二つのインスタンスの箱には owner しかありません。__init__ のやり方ではインスタンスごとに自分のリストを、最後のやり方では a だけが自分のリストを持ちます。

三つ目のやり方のように、インスタンスに同じ名前で代入すると、クラス属性はそのままで、インスタンスの __dict__ に新しい名前ができます。そのインスタンスでは、クラス属性が隠れます(5限目の、組み込みの名前を隠すのと同じ形です)。

class Case:
    size = "M"

a = Case()
b = Case()
a.size = "L"
print(a.size, b.size, vars(a))
# L M {'size': 'L'}
Case.size = "S"
print(a.size, b.size)
# L S
del a.size
print(a.size)
# S
  • クラス属性を変えると、隠していないすべてのインスタンス(b)からすぐに見えます。
  • del a.size でインスタンスの名前を外すと、またクラスの値が見えます。

+= も代入だということを忘れないでください。数値のクラス属性に += を使うと、インスタンスに新しい名前ができます。

class Gate:
    count = 0

g = Gate()
g.count += 1
print(g.count, Gate.count, vars(g))
# 1 0 {'count': 1}

g.count += 1 は g.count = g.count + 1 です。右辺はクラスから0を読み、左辺は g の __dict__ に1を入れます。すべてのインスタンスで一緒に数える数なら、Gate.count += 1 のようにクラスに直接代入しなければなりません。

ピンの案内

→ クラス本体にリストや辞書を置くと、すべてのインスタンスで共有されます。定数やデフォルト値のように本当に共有するものだけをクラスに置き、かばんごとに違うものは __init__ で self に付けましょう。

7-3. 読む順番:インスタンスの次にクラス (7分)

b.x を読むときにPythonがたどる道を、検査場の流れ図で見てみます。この回では、2段階目(インスタンスの __dict__)と4段階目(クラスの普通の属性)だけを見れば十分です。1・3・5段階目は10限目で扱います。

下の b は、size をクラスにも、自分の __dict__ にも持っています。

検査場の流れ図

1

class Case:

2

size = "M"

3

stickers = []

4

def __init__(self, owner):

5

self.owner = owner

6

def label(self):

7

return "case of " + self.owner

8

9

b = Case("cello")

10

b.size = "L"

b.

1

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

2

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

3

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

4

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

5

__getattr__がある?

6

AttributeError

結果: b. →

owner と size は2段階目のインスタンスの __dict__ で、stickers は4段階目のクラスで見つかります。label は3段階目の関数、weight はどこにもないので AttributeError です。
  • size はクラスにもありますが、2段階目で先に見つかるので 'L' です。「結果」の行のかっこ内の Case.size は、「クラスにも同じ名前がある」という印です。
  • label はクラスにある関数です。関数は3段階目に引っかかる特別なオブジェクトなので、取り出すとただの関数ではなく bound method になります。次の節のテーマです。

7-4. バウンドメソッド:取り出すたびに新しく作られる (6分)

メソッドは、クラスに入っている普通の関数です。インスタンスを通して取り出すと、Pythonが関数とインスタンスを束ねた新しいオブジェクト、つまりバウンドメソッドを作ってくれます。

class Bag:
    def __init__(self, owner):
        self.owner = owner
    def label(self):
        return "bag of " + self.owner

bag = Bag("kim")
print(bag.label is bag.label, bag.label == bag.label)
# False True
print(bag.label.__self__ is bag, bag.label.__func__ is Bag.label)
# True True
print(bag.label(), Bag.label(bag))
# bag of kim bag of kim
print(type(bag.label).__name__, type(Bag.label).__name__)
# method function
  • bag.label と書くたびに、新しいバウンドメソッドができます。だから is は False です。同じ関数と同じインスタンスを束ねているので、== は True です。
  • __self__ は束ねられたインスタンス、__func__ はクラスにある元の関数です。
  • bag.label() は Bag.label(bag) と同じです。self は、バウンドメソッドが代わりに埋めてくれる最初の引数にすぎません。
  • 関数が取り出されるときにどうやってメソッドに変わるのかは、10限目(ディスクリプタ)で解き明かします。

タグボード

ステップ 1 / 0

次に実行する行: 0

実習台を準備中…

いま変わったもの

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

m1 と m2 は別々の bound method の箱ですが、__self__ は同じインスタンスを、__func__ は class Bag にある同じ関数を指しています。

7-5. 名前を文字列で扱う:vars、getattr、setattr、hasattr (5分)

属性の名前を、実行中に文字列で決めなければならないことがあります。設定ファイルのキーや、画面で選んだ列の名前などです。

class Bag:
    def __init__(self, owner):
        self.owner = owner

bag = Bag("kim")
setattr(bag, "color", "red")
print(getattr(bag, "color"), hasattr(bag, "weight"), getattr(bag, "weight", 0))
# red False 0
print(vars(bag))
# {'owner': 'kim', 'color': 'red'}
vars(bag)["tag"] = "T1"
print(bag.tag, vars(bag) is bag.__dict__)
# T1 True
bag.weight
# AttributeError: 'Bag' object has no attribute 'weight'
  • setattr(obj, "x", v) は obj.x = v、getattr(obj, "x") は obj.x と同じです。getattr の3番目の引数は、ないときに返すデフォルト値です。
  • hasattr は、実際に getattr してみて AttributeError が出るかどうかで判断します。
  • インスタンスの vars() はスナップショットではなく、本物の __dict__ です。5限目の、関数の中の locals() とは違います。
  • Pythonのオブジェクトには、いつでも新しい属性を付けられます。便利ですが、タイプミス(bag.colour = ...)も黙って新しい属性になってしまいます。

7-6. 規格書を引き締める:__slots__ とそのほかの新機能 (5分)

__slots__ に属性の名前を書いておくと、インスタンスに __dict__ が作られず、書いた名前だけが使えます。CPythonでは、インスタンスごとの辞書がないぶんメモリも節約できます。

class Slim:
    __slots__ = ("owner",)
    def __init__(self, owner):
        self.owner = owner

s = Slim("kim")
print(s.owner, hasattr(s, "__dict__"))
# kim False
s.color = "red"
# AttributeError: 'Slim' object has no attribute 'color' and no __dict__ for setting new attributes

3.13から、クラスには __static_attributes__ があります。クラスのメソッドの中で self.名前 = ... として値を入れた名前を、並べ替えて集めておいてくれます。

class Trunk:
    def __init__(self, owner):
        self.owner = owner
        self.tags = []
    def pack(self):
        self.weight = 10

print(Trunk.__static_attributes__)
# ('owner', 'tags', 'weight')

最後に、3.14のアノテーションの遅延評価(PEP 649、749)です。クラス本体のアノテーションは、クラスを作るときではなく、__annotations__ を初めて読むときに計算されます。そのため、まだ定義していない名前をアノテーションに書いても、クラスは作られます。3.13までは friend: Missing の行で NameError になっていました。

class Partner:
    friend: Missing

print("created", Partner.__name__)
# created Partner

class Missing:
    pass

print(Partner.__annotations__)
# {'friend': <class '__main__.Missing'>}

事件解決 (10分)

ステッカーのリストを規格書からはがし、ケースごとに貼ります。共有する値(規格のサイズ)はクラスに残しておきます。

class Case:
    size = "M"
    def __init__(self, owner):
        self.owner = owner
        self.stickers = []

a = Case("violin")
b = Case("cello")
a.stickers.append("FRAGILE")
print(a.stickers, b.stickers)
# ['FRAGILE'] []
print(vars(a))
# {'owner': 'violin', 'stickers': ['FRAGILE']}
print(Case.__static_attributes__)
# ('owner', 'stickers')

これで「割れ物注意」はバイオリンケースにだけ貼られます。ムン室長が帰ったあと、ピンが13番コンベヤーの幽霊かばんの記録を画面に映します。二つのかばんは同じ規格で作られていて、片方の partner 属性はもう片方を、もう片方の partner はまたこちらを指しています。班長はその行にペンで丸を付けます。

解決報告 — LOST-007。ステッカーのリストがクラス属性だったため、すべてのケースが同じリストを共有していた。__init__ で self.stickers = [] としてケースごとに新しいリストを作って解決。

練習問題

  1. クラスに Case.stickers = [] をそのまま置いたまま a.stickers += ["FRAGILE"] を実行すると、b.stickers と vars(a) はどうなるでしょう?(ヒント:2限目の += と、7-2の += が同時に起こります。)
  2. __slots__ = ("owner",) のクラスにクラス属性 size = "M" を置いたら、インスタンスで s.size を読めるでしょうか? s.size = "L" はどうでしょう?
  3. getattr で ["owner", "color", "weight"] の三つの属性を順に取り出して表示し、ない属性は "-" と表示するループを書いてみてください。

班長の業務手帳

  • クラスもオブジェクト。type(Case) は type。class 文も名前を結び付ける代入だ。

  • インスタンスは自分の __dict__ を持つ。読むときはインスタンス → クラスの順。

  • クラス属性はすべてのインスタンスで共有。ミュータブルなオブジェクトを置くと、一か所の変更が全部から見える。

  • インスタンスに同じ名前で代入すると、クラス属性を隠す。+= も代入だ。

  • bag.m は取り出すたびに新しいバウンドメソッド(is は False、== は True)。__self__、__func__。

  • getattr / setattr / hasattr で名前を文字列として扱う。vars(obj) は本物の __dict__。

  • __slots__ は新しい属性を防ぐ。__static_attributes__(3.13)、アノテーションの遅延評価(3.14)。

← 前へ

6限目 · 機内持ち込みかばん:クロージャ

次へ →

8限目 · 空港の標準手順:ダンダーメソッド

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

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

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

質問と回答

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

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

R

ReflexKR

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

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

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

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

Built with Reflex