Kodewick
第 28 课,共 31 课目录

特殊方法

第 28 课,共 31 课 · 第 5 部分 · 面向对象 · 约 20 分钟

造一个 Lamp 对象,把它显示出来,真 Python 会显示类似 <__main__.Lamp object at 0x7f3a2c1b9e50> 的东西:类名加一个内存地址,每次运行都不一样。两盏看起来一模一样的灯用 == 比,结果是 False。你的类还不会做这些事,特殊方法来教它。

特殊方法两边各有两个下划线,比如第 26 课的 __init__,所以也有人叫它「双下划线方法」或「魔术方法」。一般不用自己调用它们:你用内置函数和运算符的时候,Python 替你调用。这里 len() 调用的就是这个类的 __len__:

main.py
1class Crate:2    def __len__(self):3        print("调用了 __len__")4        return 45 6crate = Crate()7print(len(crate))
输出
调用了 __len__
4

len(x) 运行的是 x.__len__(),a + b 运行的是 a.__add__(b),以此类推;str、int 这些内置类型也是这样。所以你自己的类只要定义了 __len__ 或 __add__,len() 和 + 就能用在它的对象上。

__str__:print 怎么显示它

__str__ 返回一段文字,print()、str() 和 f-string 显示对象时用的就是它:

main.py
1class Lamp:2    def __init__(self, street, brightness):3        self.street = street4        self.brightness = brightness5 6    def __str__(self):7        return f"{self.street}的灯(亮度 {self.brightness}/10)"8 9mill = Lamp("磨坊路", 3)10print(mill)11print(f"今晚:{mill}")12print(len(str(mill)))
输出
磨坊路的灯(亮度 3/10)
今晚:磨坊路的灯(亮度 3/10)
14

__str__ 必须返回字符串,不是把它显示出来。

__repr__:写给程序员看的

__repr__ 返回一段更精确的文字,是给程序员看的:最好长得就像造出这个对象的那行代码。需要精确的地方,Python 就用它:列表和字典里面、repr() 函数、f-string 里的 {x!r} 和 {x=}。如果一个类只写了 __repr__,print 也用它。

main.py
1class Lamp:2    def __init__(self, street, brightness):3        self.street = street4        self.brightness = brightness5 6    def __repr__(self):7        return f"Lamp({self.street!r}, {self.brightness})"8 9lamps = [Lamp("磨坊路", 3), Lamp("集市", 7)]10print(lamps)11print(lamps[0])
输出
[Lamp('磨坊路', 3), Lamp('集市', 7)]
Lamp('磨坊路', 3)

{self.street!r} 给街道名加上引号,和代码里写字符串的样子一样。点单步,看看变量面板:现在每盏灯都用它的 __repr__ 显示了。

先猜一猜

会显示什么?

1class Ore:2    def __init__(self, name):3        self.name = name4 5    def __str__(self):6        return self.name7 8    def __repr__(self):9        return f"Ore({self.name!r})"10 11gold = Ore("金")12print(gold, [gold])

小技巧: 两个只写一个的话,写 __repr__:没有 __str__ 的地方都会用它。

__eq__:两个对象什么时候相等

没有 __eq__ 时,== 问的是「这两个是不是同一个对象」。两个分开造的对象,属性一样也不相等。__eq__(self, other) 让你自己决定:

main.py
1class Plain:2    def __init__(self, name):3        self.name = name4 5class Ore:6    def __init__(self, name, weight):7        self.name = name8        self.weight = weight9 10    def __eq__(self, other):11        if not isinstance(other, Ore):12            return NotImplemented13        return self.name == other.name and self.weight == other.weight14 15print(Plain("铁") == Plain("铁"))16print(Ore("铁", 3) == Ore("铁", 3))17print(Ore("铁", 3) != Ore("金", 3))18print(Ore("铁", 3) in [Ore("金", 1), Ore("铁", 3)])19print(Ore("铁", 3) == "铁")
输出
False
True
True
True
False

!= 和 in 也用你的 __eq__。类型不一样时返回 NotImplemented,意思是「我不知道怎么跟它比」;对 == 来说,Python 最后就当作 False,而不是报错。

__lt__ 和排序

__lt__ 是「小于」,也就是 < 运算符。一个类有了它,sorted()、.sort()、min()、max() 不用 key 就能用在它的对象上:

main.py
1class Cart:2    def __init__(self, name, load):3        self.name = name4        self.load = load5 6    def __lt__(self, other):7        return self.load < other.load8 9    def __repr__(self):10        return f"Cart({self.name!r}, {self.load})"11 12carts = [Cart("A", 7), Cart("B", 2), Cart("C", 5)]13print(sorted(carts))14print(min(carts), max(carts))15print(Cart("D", 1) < Cart("E", 9))
输出
[Cart('B', 2), Cart('C', 5), Cart('A', 7)]
Cart('B', 2) Cart('A', 7)
True

Python 还能从 b < a 推出 a > b,所以只有 __lt__ 时 max 也能用。别的比较各有各的方法:__le__(<=)、__gt__(>)、__ge__(>=)。

__len__、__getitem__ 和 __contains__

这三个让你的对象像一个容器:len(x)、x[键]、东西 in x。

main.py
1class Warehouse:2    def __init__(self):3        self.stock = {}4 5    def store(self, ore, amount):6        self.stock[ore] = self.stock.get(ore, 0) + amount7 8    def __len__(self):9        return len(self.stock)10 11    def __getitem__(self, ore):12        return self.stock.get(ore, 0)13 14    def __contains__(self, ore):15        return self.stock.get(ore, 0) > 016 17depot = Warehouse()18depot.store("铁", 5)19depot.store("金", 2)20depot.store("铁", 3)21print(len(depot), depot["铁"], depot["煤"])22print("金" in depot, "煤" in depot)
输出
2 8 0
True False

__len__ 必须返回一个大于等于 0 的整数。在 __getitem__ 里,键不存在时怎么办由你决定:这里给 0,不报 KeyError。

算术运算符

每个算术运算符都有自己的方法。右边的值作为 other 传进来,一般返回一个新对象,两边原来的对象都不变:

运算符方法
a + ba.__add__(b)
a - ba.__sub__(b)
a * ba.__mul__(b)
a / ba.__truediv__(b)
a // ba.__floordiv__(b)
a % ba.__mod__(b)
main.py
1class Load:2    def __init__(self, iron, gold):3        self.iron = iron4        self.gold = gold5 6    def __add__(self, other):7        return Load(self.iron + other.iron, self.gold + other.gold)8 9    def __mul__(self, times):10        return Load(self.iron * times, self.gold * times)11 12    def __repr__(self):13        return f"Load(iron={self.iron}, gold={self.gold})"14 15morning = Load(3, 1)16evening = Load(2, 4)17print(morning + evening)18print(morning * 3)19print(morning)
输出
Load(iron=5, gold=5)
Load(iron=9, gold=3)
Load(iron=3, gold=1)

other 不一定是同一个类:这里的 __mul__ 接收的就是一个普通数字。用一个没有对应方法的运算符,会报 TypeError,比如「unsupported operand type(s) for -」。

dataclass 替你写

很多类只是装几个值,需要的 __init__、__repr__、__eq__ 写来写去都差不多。dataclasses 模块替你写好。在类上面一行写 @dataclass(@ 是装饰器,第 30 课讲),再列出字段:

main.py
1from dataclasses import dataclass2 3@dataclass4class Ore:5    name: str6    weight: int = 17 8iron = Ore("铁", 3)9print(iron)10print(iron == Ore("铁", 3))11print(Ore("金"))12print(iron.weight)
输出
Ore(name='铁', weight=3)
True
Ore(name='金', weight=1)
3

每个字段是一个名字,冒号后面写类型(类型标注是第 31 课;这里它告诉 dataclass 有哪些字段)。带 = 的字段有默认值,它们必须写在没有默认值的字段后面。列表、字典这种字段要用 field(default_factory=list),这样每个对象有自己的一份(和第 17 课可变默认值的坑是一回事)。__post_init__ 在自动生成的 __init__ 之后马上运行,用来做额外的事,这里是去掉名字两头的空格:

main.py
1from dataclasses import dataclass, field2 3@dataclass4class Cart:5    name: str6    ores: list = field(default_factory=list)7 8    def __post_init__(self):9        self.name = self.name.strip()10 11a = Cart(" 北矿车 ")12b = Cart("南矿车")13a.ores.append("铁")14print(a)15print(b)
输出
Cart(name='北矿车', ores=['铁'])
Cart(name='南矿车', ores=[])

order=True 加上 <、<=、>、>=,按字段的顺序比较,于是 sorted 能用。frozen=True 让对象只能读、不能改:

main.py
1from dataclasses import dataclass2 3@dataclass(order=True, frozen=True)4class Spot:5    x: int6    y: int7 8spots = [Spot(2, 5), Spot(1, 9), Spot(2, 1)]9print(sorted(spots))10home = Spot(0, 0)11try:12    home.x = 413except Exception as e:14    print(f"出错:{e}")
输出
[Spot(x=1, y=9), Spot(x=2, y=1), Spot(x=2, y=5)]
出错:cannot assign to field 'x'

order=True 先比 x,x 一样时才看 y。给 home.x 赋值会引发 FrozenInstanceError(这里用第 21 课的 try 接住了):冻结的对象造好以后就不能改了。适合那些不该变的值,比如地图上的一个位置。

练一练

写好代码,点检查。提示随便看,实在想不出来再看答案。

练习 1

能显示的房子

给 House 加一个 __str__ 方法,让 print(home) 显示下面这一行。

要显示
Ada 的房子,2 层
exercise1.py
1class House:2    def __init__(self, owner, floors):3        self.owner = owner4        self.floors = floors5 6home = House("Ada", 2)7print(home)
输出
点「运行」或按 Ctrl+Enter
练习 2

给灯排序

给 Lamp 加一个比较 brightness 的 __lt__ 方法,让 sorted(lamps) 从暗到亮排。按这个顺序显示街道名。

要显示
磨坊路
码头巷
集市
exercise2.py
1class Lamp:2    def __init__(self, street, brightness):3        self.street = street4        self.brightness = brightness5 6lamps = [Lamp("集市", 7), Lamp("磨坊路", 3), Lamp("码头巷", 5)]7for lamp in sorted(lamps):8    print(lamp.street)
输出
点「运行」或按 Ctrl+Enter
练习 3

用 dataclass 装金币

把 Coins 改成 dataclass,有 gold 和 silver 两个字段(都是 int),再给它一个 __add__,返回两个字段分别相加的新 Coins。显示下面的和。

要显示
Coins(gold=5, silver=15)
exercise3.py
1from dataclasses import dataclass2 3class Coins:4    pass5 6print(Coins(3, 10) + Coins(2, 5))
输出
点「运行」或按 Ctrl+Enter

这一课学会了

  • 特殊方法(双下划线方法)由 Python 在内置函数和运算符背后调用:len(x) 运行的是 x.__len__()。
  • __str__ 给 print 和 f-string 用;__repr__ 是精确的样子,列表里、没有 __str__ 时都用它。
  • __eq__ 决定 ==;没有它,== 只认同一个对象。
  • __lt__ 给出 <,sorted、min、max 有它就够了。
  • __len__、__getitem__、__contains__ 给出 len(x)、x[键] 和 in;__add__、__mul__ 这些给出算术运算。
  • @dataclass 按字段列表写好 __init__、__repr__、__eq__;order=True 加上比较,frozen=True 让对象不能改。