失物报告单
LOST-008
报告人
展会运输负责人 南主管
丢了什么
展品货箱类 Crate。把货箱放上传送带,len() 报错,for 报错,+ 也报错,print() 只打出一个看不懂的地址。
结案条件
给 Crate 装上机场标准流程(双下划线方法),让内置函数和运算符认得这个货箱。
天空岛国际机场 行李中心
南主管用自己写的类来管理展品货箱。货箱本身好好的,可一放上传送带就停住了。都美兰班长把货箱转了一圈。“传送带不看箱子的形状,它抓的是把手。这个箱子一个把手都没有。”
Python 的语法和内置函数,靠的是抓住对象身上约定好名字的方法来运作。这些名字前后各有两个下划线,所以叫“双下划线(double underscore,dunder)方法”。今天我们就把这些把手一个个装上去。
报告人的货箱是这样的。让同一个货箱做四件事试试:
class Crate:
def __init__(self, items):
self.items = list(items)
c = Crate(["vase", "lamp"])
for expr in ["len(c)", "c[0]", "c + c", "iter(c)"]:
try:
eval(expr)
except TypeError as e:
print(f"{expr:7} → {e}")
# len(c) → object of type 'Crate' has no len()
# c[0] → 'Crate' object is not subscriptable
# c + c → unsupported operand type(s) for +: 'Crate' and 'Crate'
# iter(c) → 'Crate' object is not iterablelen(c) 不是魔法。Python 调用的是 type(c).__len__(c)。没有这个方法,就报“没有 len”。装上 __len__,马上就行了。
class Crate:
def __init__(self, items):
self.items = list(items)
def __len__(self):
return len(self.items)
c = Crate(["vase", "lamp"])
print(len(c), type(c).__len__(c))
# 2 2
c.__len__ = lambda: 99
print(len(c), c.__len__())
# 2 99最后两行很重要。即使给实例挂上 __len__,len(c) 仍然是 2。因为语法调用的双下划线方法是在类型(类)上找的,而不是在实例上。传送带只认规格书上写着的流程。
| 语法 | 实际调用的 |
|---|---|
len(x) | type(x).__len__(x) |
x[i]、x[i] = v | __getitem__、__setitem__ |
a + b | a.__add__(b),不行再 b.__radd__(a) |
a == b | __eq__ |
v in x | __contains__(没有就逐个迭代比较) |
for v in x | __iter__(没有就用 __getitem__) |
bool(x)、if x: | __bool__(没有就用 __len__) |
print(x)、str(x) | __str__(没有就用 __repr__) |
x() | __call__ |
with x: | __enter__、__exit__ |
把这些把手装好的类,通常被称为“Pythonic 的类”。因为不用另外记 size()、merge() 这类方法,用早已熟悉的 len 和 + 就能操作它。
下面的调度台会把一行语法交给装好把手的 Crate,再按顺序显示 Python 实际调用了哪些方法。
双下划线调度台
1
class Crate:
2
def __init__(self, items):
3
self.items = list(items)
4
def __repr__(self):
5
return f"Crate({self.items})"
6
def __len__(self):
7
return len(self.items)
8
def __getitem__(self, i):
9
return self.items[i]
10
def __add__(self, other):
11
return Crate(self.items + other.items)
12
13
c = Crate(["vase", "lamp"])
14
d = Crate(["cup"])
bool(c) 没有 __bool__,所以改问 __len__。长度不为 0 就是真。for x in c 为什么会调用 __getitem__,8-3 里再看。小钉提示
→ 双下划线方法只管装上,调用时别写 c.__len__(),而是像 len(c) 这样用语法来调用。语法会去类型上找,找不到时还会帮你走别的路(转给 __radd__、用 __len__ 代替 __bool__)。
__repr__ 与 __str__(7 分钟)报告人为了调试把货箱打印出来,只看到这样一串字:
class Crate:
def __init__(self, items):
self.items = list(items)
print(Crate(["vase"]))
# 例如:<__main__.Crate object at 0x000001F4C2A8B2C0>object 继承下来的默认 __repr__ 只有类型名和地址,看不出箱子里装了什么。把对象变成文字的把手有两个:
| 给谁看的文字 | 用在哪里 | |
|---|---|---|
__repr__ | 开发者。尽量写成 Crate([...]) 这种能重新创建对象的样子 | repr()、交互式 shell、调试器、列表和 dict 里的元素、f-string 的 !r |
__str__ | 用户。读起来顺畅的句子 | print()、str()、f-string 的 {x} |
class Crate:
def __init__(self, items):
self.items = list(items)
def __repr__(self):
return f"Crate({self.items})"
def __str__(self):
return f"crate of {len(self.items)}"
c = Crate(["vase", "lamp"])
print(c)
# crate of 2
print(repr(c))
# Crate(['vase', 'lamp'])
print([c])
# [Crate(['vase', 'lamp'])]
print(f"{c} / {c!r}")
# crate of 2 / Crate(['vase', 'lamp'])print([c]) 用的不是 __str__,而是 __repr__。把列表转成文字时,元素总是用 repr 来写。__str__ 时,str() 和 print() 会用 __repr__;反过来则不行。所以如果只装一个,先装 __repr__。repr。追 bug 时看到的样子,就是 __repr__。双下划线调度台
1
class Crate:
2
def __init__(self, items):
3
self.items = list(items)
4
def __repr__(self):
5
return f"Crate({self.items})"
6
def __str__(self):
7
return f"crate of {len(self.items)}"
8
9
c = Crate(["vase", "lamp"])
要让货箱像容器一样工作,五个把手就够了:__len__、__getitem__、__iter__、__contains__、__bool__。可在 8-1 里,明明没有 __iter__,for 却也跑起来了。
class OldCrate:
def __init__(self, items):
self.items = list(items)
def __getitem__(self, i):
print("getitem", i)
return self.items[i]
for x in OldCrate(["vase", "lamp"]):
pass
# getitem 0
# getitem 1
# getitem 2
print("lamp" in OldCrate(["vase", "lamp"]))
# getitem 0
# getitem 1
# True没有 __iter__ 时,iter() 会生成一个按 0、1、2… 顺序调用 __getitem__ 的迭代器。一遇到 IndexError,就当作“结束了”,悄悄停下。所以打印到 getitem 2 后没有报错就结束了。没有 __contains__ 时,in 就这样一个个取出来用 == 比较,找到了就停。
这条路是为了兼容老代码而保留的。新写的类,就把把手好好装上。
class Crate:
def __init__(self, items):
self.items = list(items)
def __len__(self):
return len(self.items)
def __iter__(self):
yield from self.items
def __contains__(self, x):
return x in self.items
c, e = Crate(["vase", "lamp"]), Crate([])
print(list(c), "lamp" in c, bool(c), bool(e))
# ['vase', 'lamp'] True True False
if not e:
print("empty crate")
# empty crate__iter__ 必须返回一个迭代器。可以写 return iter(self.items),也可以像上面那样用 yield,函数就成了生成器,自动生成迭代器。生成器会在生成器与装饰器的课程里深入讲解。__contains__ 时,in 不再迭代,而是直接问这个方法。__bool__ 时,按 __len__ 是否为 0 来决定真假。两个都没有的话,对象永远为真。所以空的 Crate 只要装上 __len__,if not e: 就能自然地工作。双下划线调度台
1
class Crate:
2
def __init__(self, items):
3
self.items = list(items)
4
def __len__(self):
5
return len(self.items)
6
def __iter__(self):
7
yield from self.items
8
def __contains__(self, x):
9
return x in self.items
10
11
c = Crate(["vase", "lamp"])
12
e = Crate([])
__add__、__radd__、NotImplemented(8 分钟)a + b 分两步展开:
type(a).__add__(a, b)。NotImplemented,意思是“我不认识这个搭档”。Python 就交给对方的 type(b).__radd__(b, a)。NotImplemented,就是 TypeError。class Crate:
def __init__(self, items):
self.items = list(items)
def __repr__(self):
return f"Crate({self.items})"
def __add__(self, other):
if not isinstance(other, Crate):
return NotImplemented
return Crate(self.items + other.items)
def __radd__(self, other):
if other == 0:
return self
return NotImplemented
c, d = Crate(["vase"]), Crate(["cup"])
print(c + d)
# Crate(['vase', 'cup'])
print(sum([c, d]))
# Crate(['vase', 'cup'])
c + 1
# TypeError: unsupported operand type(s) for +: 'Crate' and 'int'sum() 从 0 开始,先算 0 + c。int.__add__(0, c) 不认识货箱,返回 NotImplemented,于是调用 Crate.__radd__(c, 0)。__radd__ 接住了 0,sum 就成立了。NotImplemented,这样对方才有机会处理。它和 NotImplementedError(异常)是两回事。双下划线调度台
1
class Crate:
2
def __init__(self, items):
3
self.items = list(items)
4
def __repr__(self):
5
return f"Crate({self.items})"
6
def __add__(self, other):
7
if not isinstance(other, Crate):
8
return NotImplemented
9
return Crate(self.items + other.items)
10
def __radd__(self, other):
11
if other == 0:
12
return self
13
return NotImplemented
14
15
c = Crate(["vase"])
16
d = Crate(["cup"])
第 2 课看到的 += 的两副面孔,在这里揭开了谜底。a += b 先找 __iadd__(就地加法),没有的话就改成 a = a + b,把标签移到新对象上。list 有 __iadd__,tuple 和 str 没有。
c = Crate(["vase"])
alias = c
c += Crate(["cup"])
print(c is alias, alias)
# False Crate(['vase'])
class Crate2(Crate):
def __iadd__(self, other):
self.items += other.items
return self
c = Crate2(["vase"])
alias = c
c += Crate2(["cup"])
print(c is alias, alias)
# True Crate(['vase', 'cup'])有 __iadd__ 时,它改动箱子里面,并返回自己。Python 会把 c 标签重新挂到这个返回值上,因为是同一个箱子,标签等于没动。(class Crate2(Crate) 是继承 Crate 的规格书,那是第 9 课的主题。)
小钉提示
→ 要是在 __iadd__ 里漏写 return self,c += d 之后 c 就变成 None 了。因为 Python 会把标签重新挂到 __iadd__ 返回的东西上。
__call__、with、@dataclass(7 分钟)装上 __call__,对象就能像函数一样调用。需要一个随身带着设定值的函数时就用它(用类来做第 6 课闭包做的事)。
class Scale:
def __init__(self, limit):
self.limit = limit
def __call__(self, kg):
return kg <= self.limit
check = Scale(23)
print(check(20), check(30), callable(check))
# True False Truewith 会调用 __enter__ 和 __exit__。即使代码块里抛出异常,__exit__ 也一定会被调用。这就是它被用来清理资源的原因,第 11 课还会再见到它。
class Belt:
def __enter__(self):
print("belt on")
return self
def __exit__(self, exc_type, exc, tb):
print("belt off", exc_type)
return False
with Belt() as b:
print("moving")
# belt on
# moving
# belt off None__exit__ 返回 True 会吞掉异常。大多数情况下,返回 False(或者什么都不返回)才是对的。
__init__、__repr__、__eq__ 这类写法总是大同小异的把手,可以交给 @dataclass 来生成。
from dataclasses import dataclass
@dataclass
class Tag:
code: str
kg: int = 0
t = Tag("ICN", 12)
print(t, t == Tag("ICN", 12))
# Tag(code='ICN', kg=12) True
print(Tag.__hash__)
# None看最后一行。一旦定义了 __eq__,Python 就会把 __hash__ 设成 None。自己写的类也一样。
class Crate:
def __init__(self, items):
self.items = list(items)
def __eq__(self, other):
return isinstance(other, Crate) and self.items == other.items
print(Crate(["vase"]) == Crate(["vase"]), Crate.__hash__)
# True None
{Crate(["vase"])}
# TypeError: cannot use 'Crate' as a set element (unhashable type: 'Crate')既然规定了“值相同就相等”,哈希却还按身份(地址)来算,那么两个相等的货箱就会进到 set 的不同格子里。为了不破坏这个约定,干脆把哈希关掉了。哈希与相等的约定,第 12 课会详细讲。
这是装齐了把手的 Crate:
class Crate:
def __init__(self, items=()):
self.items = list(items)
def __repr__(self):
return f"Crate({self.items})"
def __len__(self):
return len(self.items)
def __getitem__(self, i):
return self.items[i]
def __iter__(self):
return iter(self.items)
def __contains__(self, x):
return x in self.items
def __add__(self, other):
if not isinstance(other, Crate):
return NotImplemented
return Crate(self.items + other.items)
def __radd__(self, other):
return self if other == 0 else NotImplemented
a, b = Crate(["vase", "lamp"]), Crate(["frame"])
print(len(a), a[0], "lamp" in a, [x.upper() for x in a])
# 2 vase True ['VASE', 'LAMP']
print(a + b, sum([a, b]))
# Crate(['vase', 'lamp', 'frame']) Crate(['vase', 'lamp', 'frame'])
print(a[-1], a[:1])
# lamp ['vase']a[:1] 把一个切片对象传给了 __getitem__,于是原样返回了列表的切片。想让它返回货箱的话,用 isinstance(i, slice) 分开处理就行。
南主管的货箱安安静静地从传送带上滑了过去。班长说:“样子一点没变,只是装上了把手。”
结案报告 — LOST-008。Crate 没有把手(双下划线方法),语法认不出这个货箱。装上 repr、len、getitem、iter、contains、add、radd 后解决。
Crate 加上 __eq__,看看 {Crate(["vase"])} 会怎样。用一句话写下原因。__bool__ 总是返回 False 的类,当 __len__ 为 3 时,预测 if obj: 听哪一个的,再运行确认。__getitem__,当 isinstance(i, slice) 时返回 Crate,让 a[:1] 变成 Crate(['vase'])。班长的工作手册
语法调用双下划线方法。len(x) 就是 type(x).__len__(x)。在类型上找,而不是在实例上。
只装一个就装 __repr__。print 没有 __str__ 时用 __repr__,列表里的元素总是用 repr。
for 用 __iter__,没有就从 0 开始调用 __getitem__(遇到 IndexError 停下)。bool 用 __bool__,没有就用 __len__。
遇到不认识的搭档,返回 NotImplemented。这样对方的 __radd__ 才有机会。
a += b 有 __iadd__ 就就地修改(return self),没有就是 a = a + b。
定义了 __eq__,__hash__ 就是 None。@dataclass 也一样。
订阅新课时和新课程通知
本课程新增课时或推出后续课程时,我们会发邮件通知你。
问答
关于本课有任何疑问都可以在这里留言。所有语言版本页面的提问会汇集在一起。