失物报告单
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 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。它适合做继承之后也能造出正确规格的“替代构造器”。
读取 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__。小钉提示
→ 把顺序记成一行:“数据描述符 > 实例 __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: -10用 setattr 放进去、新创建、用 -= 减少,统统被拦下了。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。
订阅新课时和新课程通知
本课程新增课时或推出后续课程时,我们会发邮件通知你。
问答
关于本课有任何疑问都可以在这里留言。所有语言版本页面的提问会汇集在一起。