失物报告单
LOST-007
报告人
管弦乐团巡演运输负责人 文经理
丢了什么
只在一个乐器箱上贴了“易碎品”贴纸,可按同一规格做的所有乐器箱上都出现了这张贴纸。
结案条件
找出贴纸清单实际放在哪里,并让每个乐器箱都有自己的贴纸清单。
天空岛国际机场 行李中心
都美兰班长的抽屉里有一沓箱子规格书。用一张规格书,同样形状的箱子要做多少个就能做多少个。“规格书是一张纸,箱子是一个个箱子。两样都是东西。”
文经理说,自己只是在小提琴箱上贴了“易碎品”,大提琴箱上却也出现了同样的贴纸。班长没去看箱子,而是翻开了规格书。贴纸就贴在那里。
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" 这样直接给类赋值。这是报告人的规格书:
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 Falsestickers = [] 在类体里只执行一次。列表只是在规格书上贴了一个。a.stickers 在 a 的 __dict__ 里找不到,于是去类里找;append 改动的就是这唯一的列表(改动箱子)。每个实例都该单独拥有的东西,要在 __init__ 里挂到 self 上。在下面比较三种写法。
标签板
步骤 1 / 0
下一行将执行: 0
正在准备实验台…
刚刚变化的
封口的箱子(不可变)
像第三种写法那样,用同一个名字给实例赋值,类属性保持不变,而实例的 __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)
# Sb)立刻都能看到。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 上。
用安检流程图来看读取 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.
类上有数据描述符(__get__ + __set__)吗?
在实例的 __dict__ 里吗?
类上有非数据描述符(像函数一样只有 __get__)吗?
类上有普通属性吗?
有 __getattr__ 吗?
AttributeError
结果: b. →
size 类上也有,但第 2 步先找到了,所以是 'L'。结果行括号里的 Case.size 表示“类上也有同名属性”。label 是类上的函数。函数是会在第 3 步被拦下的特殊对象,取出来时不再是普通函数,而是 bound method。这是下一节的主题。方法就是放在类里的普通函数。通过实例取出它时,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 functionbag.label,就生成一个新的绑定方法,所以 is 是 False。但绑的是同一个函数、同一个实例,所以 == 是 True。__self__ 是绑定的实例,__func__ 是类里原来的函数。bag.label() 等同于 Bag.label(bag)。self 不过是绑定方法替你填好的第一个参数。标签板
步骤 1 / 0
下一行将执行: 0
正在准备实验台…
刚刚变化的
封口的箱子(不可变)
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() 不同。bag.colour = ...)也会悄无声息地变成新属性。__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'>}把贴纸清单从规格书上揭下来,贴到每个乐器箱上。要共用的值(规格尺寸)留在类上。
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 = [] 为每个箱子新建列表,问题解决。
Case.stickers = [],执行 a.stickers += ["FRAGILE"],b.stickers 和 vars(a) 会怎样?(提示:第 2 课的 += 和 7-2 的 += 同时发生。)__slots__ = ("owner",) 的类上放一个类属性 size = "M",能从实例读取 s.size 吗?s.size = "L" 呢?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)。
订阅新课时和新课程通知
本课程新增课时或推出后续课程时,我们会发邮件通知你。
问答
关于本课有任何疑问都可以在这里留言。所有语言版本页面的提问会汇集在一起。