类和对象
到现在为止,数据都放在字符串、数字、列表和字典里。类(class)让你造一种自己的类型。下面是一个 Lamp(灯)类型:每盏灯有所在的街道和亮度,每盏灯都能打开。
1class Lamp:2 def __init__(self, street, brightness):3 self.street = street4 self.brightness = brightness5 self.on = False6 7 def switch_on(self):8 self.on = True9 print(f"{self.street}的灯亮了")10 11mill = Lamp("磨坊路", 3)12mill.switch_on()13print(mill.brightness, mill.on)
磨坊路的灯亮了 3 True
类和对象
类是图纸;照着图纸造出来的每一样东西是一个对象(也叫实例)。Lamp("磨坊路", 3) 造出一盏新灯,mill 是它的名字。再调用一次类,就得到另一盏、互不相干的灯:
1class Lamp:2 def __init__(self, street, brightness):3 self.street = street4 self.brightness = brightness5 6mill = Lamp("磨坊路", 3)7market = Lamp("集市", 5)8print(mill.street, mill.brightness)9print(market.street, market.brightness)10print(type(mill))
磨坊路 3 集市 5 <class '__main__.Lamp'>
存在对象上的值,比如 mill.street,叫它的属性,用点来读。类名习惯每个单词首字母大写(Lamp、BankAccount),和变量名、函数名不一样。
如果直接显示对象本身,比如 print(mill),真 Python 会显示类似 <__main__.Lamp object at 0x7f3a...> 的东西:类名加一个内存地址,每次运行都不一样。这没什么用;第 28 课教你让对象显示得好看。
__init__ 给新对象做准备
__init__(两边各两个下划线)是一个特殊方法,每造一个对象,它就自动跑一次。它的任务是给新对象设好一开始的属性。你传给 Lamp(...) 的实参,会交给 __init__ 里 self 后面的那些参数。
少给一个,报错和普通函数一样:
1class Lamp:2 def __init__(self, street, brightness):3 self.street = street4 self.brightness = brightness5 6dock = Lamp("码头巷")
Traceback (most recent call last):
File "main.py", line 6, in <module>
dock = Lamp("码头巷")
TypeError: Lamp.__init__() missing 1 required positional argument: 'brightness'发生了什么: 函数要的参数个数不对,多给了或者少给了。
__init__ 也可以设不来自实参的属性,比如第一个例子里的 self.on = False:每盏灯一开始都是关着的。
self 就是这个对象
每个方法的第一个参数,都是调用它的那个对象,习惯上叫 self。写 mill.switch_on() 时,Python 运行 switch_on,self 就是 mill。所以 self.on = True 只改这一盏灯,别的灯不变。
最后一行会显示什么?
1class Lamp:2 def __init__(self, street):3 self.street = street4 self.on = False5 6 def switch_on(self):7 self.on = True8 9mill = Lamp("磨坊路")10market = Lamp("集市")11market.switch_on()12print(mill.on, market.on)
market.switch_on() 运行方法时 self 是 market,所以只有 market.on 变成 True。mill 有自己的 on,还是 False。
小技巧: 这一课的每段代码都可以点单步。变量面板里能看到每个对象和它的属性;方法运行的时候,self 就是调用这个方法的那个对象。
方法
方法就是写在类里面的函数。和第 16 课的函数一样,方法可以在 self 后面再要别的参数,可以用对象的属性,也可以返回值:
1class Lamp:2 def __init__(self, street, brightness):3 self.street = street4 self.brightness = brightness5 6 def brighten(self, amount):7 self.brightness = min(self.brightness + amount, 10)8 9 def label(self):10 return f"{self.street}:{self.brightness}/10"11 12mill = Lamp("磨坊路", 3)13mill.brighten(4)14print(mill.label())15mill.brighten(6)16print(mill.label())
磨坊路:7/10 磨坊路:10/10
方法里也能通过 self 调用同一个对象的别的方法,比如 self.brighten(1)。常见的错误是方法的参数里忘了写 self:Python 照样把对象传进去,却没地方放。
1class Lamp:2 def switch_on():3 print("开")4 5mill = Lamp()6mill.switch_on()
Traceback (most recent call last):
File "main.py", line 6, in <module>
mill.switch_on()
TypeError: Lamp.switch_on() takes 0 positional arguments but 1 was given发生了什么: 函数要的参数个数不对,多给了或者少给了。
报错说这个方法要 0 个实参,却给了 1 个:这 1 个就是对象本身。这个类没有 __init__,这没关系:造出来的对象一开始没有属性而已。
改属性
在外面直接赋值就能改属性,甚至能加新的。读一个从来没设过的属性,会报 AttributeError:
1class Lamp:2 def __init__(self, street):3 self.street = street4 5mill = Lamp("磨坊路")6mill.street = "老磨坊路"7mill.color = "黄色"8print(mill.street, mill.color)9print(mill.brightness)
老磨坊路 黄色
Traceback (most recent call last):
File "main.py", line 9, in <module>
print(mill.brightness)
AttributeError: 'Lamp' object has no attribute 'brightness'发生了什么: 这种东西没有「brightness」这个方法。
直接改属性是可以的,但用方法来改往往更好:方法可以先检查新值对不对。下一个例子就是这样。
一个银行账户
这个类记着余额,不让余额变成负数。和钱有关的规矩都写在一个地方,就在类里面:
1class BankAccount:2 def __init__(self, owner, balance=0):3 self.owner = owner4 self.balance = balance5 6 def deposit(self, amount):7 if amount <= 0:8 raise ValueError("存款必须是正数")9 self.balance += amount10 11 def withdraw(self, amount):12 if amount > self.balance:13 print(f"{self.owner}:余额不够")14 return False15 self.balance -= amount16 return True17 18wick = BankAccount("Wick", 20)19wick.deposit(15)20wick.withdraw(50)21wick.withdraw(30)22print(f"{wick.owner} 还有 {wick.balance} 枚金币")
Wick:余额不够 Wick 还有 5 枚金币
balance=0 是默认值,和第 17 课一样:BankAccount("Ada") 从 0 开始。raise 是第 21 课讲的。
类属性
设在 self 上的属性属于一个对象。直接写在类里面的属性属于类,所有对象共用。适合放大家都一样的值,或者用来数一共造了几个对象:
1class Lamp:2 max_brightness = 103 count = 04 5 def __init__(self, street):6 self.street = street7 Lamp.count += 18 9mill = Lamp("磨坊路")10market = Lamp("集市")11print(Lamp.count)12print(mill.max_brightness, market.max_brightness)
2 10 10
读 mill.max_brightness 时,Python 先在对象上找,找不到再去类上找。要改类属性,要通过类来赋值,比如 Lamp.count += 1。
最后一行会显示什么?
1class Lamp:2 max_brightness = 103 4mill = Lamp()5market = Lamp()6mill.max_brightness = 57print(market.max_brightness)
mill.max_brightness = 5 只在 mill 上新建了一个属性,对 mill 来说挡住了类上的那个。market 自己没有,所以还是找到类属性 10。
把对象放进列表
对象和别的值一样,可以放进列表。然后循环、推导式、sum、带 key 的 sorted(第 19 课)都能用在它们身上:
1class Lamp:2 def __init__(self, street, brightness):3 self.street = street4 self.brightness = brightness5 6lamps = [Lamp("磨坊路", 3), Lamp("集市", 7), Lamp("码头巷", 5)]7for lamp in lamps:8 print(lamp.street, lamp.brightness)9 10print("总亮度", sum([lamp.brightness for lamp in lamps]))11brightest = max(lamps, key=lambda lamp: lamp.brightness)12print("最亮的是", brightest.street)13dim = [lamp.street for lamp in lamps if lamp.brightness < 6]14print(dim)
磨坊路 3 集市 7 码头巷 5 总亮度 15 最亮的是 集市 ['磨坊路', '码头巷']
练一练
写好代码,点检查。提示随便看,实在想不出来再看答案。
一座房子
✓写一个类 House,它的 __init__ 接收 owner(房主)和 floors(层数)并存起来。然后给「Ada」造一座 2 层的房子,显示它的两个属性。
Ada 2
1class House:2 # 在这里写3 pass4 5home = House("Ada", 2)
计数器
✓给 Tally 加两个方法:add(self, n) 把 n 加到 self.total 上;report(self) 返回「合计:」加上总数。程序最后会显示下面这一行。
合计:13
1class Tally:2 def __init__(self):3 self.total = 04 5ores = Tally()6ores.add(4)7ores.add(9)8print(ores.report())
数矿车
✓用一个类属性 made 数一共造了几辆 Cart(矿车)。造完下面三辆以后,显示 Cart.made。
3
1class Cart:2 def __init__(self, load):3 self.load = load4 5a = Cart(3)6b = Cart(5)7c = Cart(2)
这一课学会了
class 名字:定义一种新类型;调用名字(...)就造出一个这种类型的对象。__init__在造对象时运行,把一开始的属性设在self上。self是调用方法的那个对象:mill.switch_on()运行switch_on时,self就是mill。- 方法是类里面的函数,可以读、可以改对象的属性。
self上的属性属于一个对象;类属性所有对象共用。- 对象可以放进列表:循环、按某个属性求和、用
key排序都行。