R

ReflexKR

Reflex 官方文档

名牌机场:Python 对象模型

·

课程概览

1

标签不是箱子:名字与对象

2

封口的箱子:可变与不可变

3

箱中箱:容器与复制

4

交出箱子的时候:函数与参数

5

区域与通行证:作用域

6

随身行李:闭包

7

箱子规格书:类与实例

8

机场标准流程:双下划线方法

9

规格书的规格书:继承与 MRO

10

安检口:属性访问的秘密

11

无主的箱子:内存与 GC

12

储物柜号码:哈希与相等,综合

名牌机场: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 课的默认参数是同一种 bug:“一次创建的可变对象被多方共用。”

每个实例都该单独拥有的东西,要在 __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,左边把 1 放进 g 的 __dict__。如果是所有实例一起累加的数字,就得像 Gate.count += 1 这样直接给类赋值。

小钉提示

→ 在类体里放列表或字典,所有实例都会共用它。只把常量、默认值这类真正要共用的放在类上;每个箱子各不相同的,在 __init__ 里挂到 self 上。

7-3. 读取顺序:先实例,后类(7 分钟)

用安检流程图来看读取 b.x 时 Python 走过的路。这节课只需要看第 2 步(实例 __dict__)和第 4 步(类上的普通属性)。第 1、3、5 步在第 10 课讲。

下面的 b,在类上和自己的 __dict__ 里都有 size。

安检流程图

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 的第三个参数是找不到时返回的默认值。
  • 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