R

ReflexKR

Reflex 官方文档

名牌机场:Python 对象模型

·

课程概览

1

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

2

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

3

箱中箱:容器与复制

4

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

5

区域与通行证:作用域

6

随身行李:闭包

7

箱子规格书:类与实例

8

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

9

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

10

安检口:属性访问的秘密

11

无主的箱子:内存与 GC

12

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

名牌机场:Python 对象模型
第8课

时间 5:50–6:40

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

失物报告单

LOST-008

传送带认不出的货箱

报告人

展会运输负责人 南主管

丢了什么

展品货箱类 Crate。把货箱放上传送带,len() 报错,for 报错,+ 也报错,print() 只打出一个看不懂的地址。

结案条件

给 Crate 装上机场标准流程(双下划线方法),让内置函数和运算符认得这个货箱。

天空岛国际机场 行李中心

南主管用自己写的类来管理展品货箱。货箱本身好好的,可一放上传送带就停住了。都美兰班长把货箱转了一圈。“传送带不看箱子的形状,它抓的是把手。这个箱子一个把手都没有。”

Python 的语法和内置函数,靠的是抓住对象身上约定好名字的方法来运作。这些名字前后各有两个下划线,所以叫“双下划线(double underscore,dunder)方法”。今天我们就把这些把手一个个装上去。

8-1. 语法调用的是双下划线方法(9 分钟)

报告人的货箱是这样的。让同一个货箱做四件事试试:

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 iterable

len(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 + ba.__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"])

c + d 要在 __add__ 里新建一个货箱,所以 __init__ 先执行完。print(c) 没有 __str__,于是调用 __repr__。for 没有 __iter__,于是按 0、1 的顺序调用 __getitem__,到 2 时抛出 IndexError 就停下(列表最后一行的 raise IndexError)。
  • bool(c) 没有 __bool__,所以改问 __len__。长度不为 0 就是真。
  • for x in c 为什么会调用 __getitem__,8-3 里再看。

小钉提示

→ 双下划线方法只管装上,调用时别写 c.__len__(),而是像 len(c) 这样用语法来调用。语法会去类型上找,找不到时还会帮你走别的路(转给 __radd__、用 __len__ 代替 __bool__)。

8-2. __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"])

print(c) 和 f'{c}' 调用 __str__,repr(c) 和 f'{c!r}' 调用 __repr__。print([c]) 中 c 是列表里的元素,所以调用 __repr__。

8-3. 容器协议(9 分钟)

要让货箱像容器一样工作,五个把手就够了:__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([])

in 直接找 __contains__,for 和 max 直接找 __iter__。list(c) 为了预先分配空间,还会问一下 __len__(CPython)。bool(e) 没有 __bool__,于是调用 __len__。

8-4. 运算符:__add__、__radd__、NotImplemented(8 分钟)

a + b 分两步展开:

  1. 调用 type(a).__add__(a, b)。
  2. 如果结果是 NotImplemented,意思是“我不认识这个搭档”。Python 就交给对方的 type(b).__radd__(b, a)。
  3. 两边都返回 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"])

0 + c 中 int 不认识货箱,于是转给 Crate.__radd__。c + 1 中 __add__ 返回 NotImplemented,int 也不认识,于是 TypeError。c += d 没有 __iadd__,于是用 __add__ 新建一个货箱。

第 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__ 返回的东西上。

8-5. __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 True

with 会调用 __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 课会详细讲。

结案(10 分钟)

这是装齐了把手的 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 后解决。

课后练习

  1. 给结案时的 Crate 加上 __eq__,看看 {Crate(["vase"])} 会怎样。用一句话写下原因。
  2. 写一个 __bool__ 总是返回 False 的类,当 __len__ 为 3 时,预测 if obj: 听哪一个的,再运行确认。
  3. 修改 __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 也一样。

← 上一课

第7课 · 箱子规格书:类与实例

下一课 →

第9课 · 规格书的规格书:继承与 MRO

订阅新课时和新课程通知

本课程新增课时或推出后续课程时,我们会发邮件通知你。

收集项目:电子邮箱 · 用途:本课程的新课时和上线通知 · 保存期限:直到你要求删除。不会用于其他用途,也不会提供给他人。回复任意通知邮件,我们会立即删除。

问答

关于本课有任何疑问都可以在这里留言。所有语言版本页面的提问会汇集在一起。

以纯文本显示,不支持链接和格式。

R

ReflexKR

动手学 Python 和 Reflex。所有示例均已在 Reflex 0.10.0 上实际运行验证。

ReflexKR 免费,没有横幅广告。如果对你有帮助,欢迎请我们喝杯咖啡。 ☕ 请我们喝杯咖啡

联盟链接与赞助说明
한국어English日本語简体中文EspañolDeutsch

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

Built with Reflex