特殊方法
造一个 Lamp 对象,把它显示出来,真 Python 会显示类似 <__main__.Lamp object at 0x7f3a2c1b9e50> 的东西:类名加一个内存地址,每次运行都不一样。两盏看起来一模一样的灯用 == 比,结果是 False。你的类还不会做这些事,特殊方法来教它。
特殊方法两边各有两个下划线,比如第 26 课的 __init__,所以也有人叫它「双下划线方法」或「魔术方法」。一般不用自己调用它们:你用内置函数和运算符的时候,Python 替你调用。这里 len() 调用的就是这个类的 __len__:
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 显示对象时用的就是它:
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 也用它。
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])
print 显示 gold 本身时用 __str__,得到 金。列表显示里面的东西时用 __repr__,得到 [Ore('金')]。
小技巧: 两个只写一个的话,写 __repr__:没有 __str__ 的地方都会用它。
__eq__:两个对象什么时候相等
没有 __eq__ 时,== 问的是「这两个是不是同一个对象」。两个分开造的对象,属性一样也不相等。__eq__(self, other) 让你自己决定:
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 就能用在它的对象上:
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。
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 + b | a.__add__(b) |
a - b | a.__sub__(b) |
a * b | a.__mul__(b) |
a / b | a.__truediv__(b) |
a // b | a.__floordiv__(b) |
a % b | a.__mod__(b) |
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 课讲),再列出字段:
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__ 之后马上运行,用来做额外的事,这里是去掉名字两头的空格:
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 让对象只能读、不能改:
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 接住了):冻结的对象造好以后就不能改了。适合那些不该变的值,比如地图上的一个位置。
练一练
写好代码,点检查。提示随便看,实在想不出来再看答案。
能显示的房子
✓给 House 加一个 __str__ 方法,让 print(home) 显示下面这一行。
Ada 的房子,2 层
1class House:2 def __init__(self, owner, floors):3 self.owner = owner4 self.floors = floors5 6home = House("Ada", 2)7print(home)
给灯排序
✓给 Lamp 加一个比较 brightness 的 __lt__ 方法,让 sorted(lamps) 从暗到亮排。按这个顺序显示街道名。
磨坊路 码头巷 集市
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)
用 dataclass 装金币
✓把 Coins 改成 dataclass,有 gold 和 silver 两个字段(都是 int),再给它一个 __add__,返回两个字段分别相加的新 Coins。显示下面的和。
Coins(gold=5, silver=15)
1from dataclasses import dataclass2 3class Coins:4 pass5 6print(Coins(3, 10) + Coins(2, 5))
这一课学会了
- 特殊方法(双下划线方法)由 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让对象不能改。