Kodewick
第 26 课,共 31 课目录

类和对象

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

到现在为止,数据都放在字符串、数字、列表和字典里。类(class)让你造一种自己的类型。下面是一个 Lamp(灯)类型:每盏灯有所在的街道和亮度,每盏灯都能打开。

main.py
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 是它的名字。再调用一次类,就得到另一盏、互不相干的灯:

main.py
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 后面的那些参数。

少给一个,报错和普通函数一样:

main.py
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)

小技巧: 这一课的每段代码都可以点单步。变量面板里能看到每个对象和它的属性;方法运行的时候,self 就是调用这个方法的那个对象。

方法

方法就是写在类里面的函数。和第 16 课的函数一样,方法可以在 self 后面再要别的参数,可以用对象的属性,也可以返回值:

main.py
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 照样把对象传进去,却没地方放。

main.py
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:

main.py
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」这个方法。

直接改属性是可以的,但用方法来改往往更好:方法可以先检查新值对不对。下一个例子就是这样。

一个银行账户

这个类记着余额,不让余额变成负数。和钱有关的规矩都写在一个地方,就在类里面:

main.py
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 上的属性属于一个对象。直接写在类里面的属性属于类,所有对象共用。适合放大家都一样的值,或者用来数一共造了几个对象:

main.py
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)

把对象放进列表

对象和别的值一样,可以放进列表。然后循环、推导式、sum、带 key 的 sorted(第 19 课)都能用在它们身上:

main.py
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
最亮的是 集市
['磨坊路', '码头巷']

练一练

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

练习 1

一座房子

写一个类 House,它的 __init__ 接收 owner(房主)和 floors(层数)并存起来。然后给「Ada」造一座 2 层的房子,显示它的两个属性。

要显示
Ada 2
exercise1.py
1class House:2    # 在这里写3    pass4 5home = House("Ada", 2)
输出
点「运行」或按 Ctrl+Enter
练习 2

计数器

给 Tally 加两个方法:add(self, n) 把 n 加到 self.total 上;report(self) 返回「合计:」加上总数。程序最后会显示下面这一行。

要显示
合计:13
exercise2.py
1class Tally:2    def __init__(self):3        self.total = 04 5ores = Tally()6ores.add(4)7ores.add(9)8print(ores.report())
输出
点「运行」或按 Ctrl+Enter
练习 3

数矿车

用一个类属性 made 数一共造了几辆 Cart(矿车)。造完下面三辆以后,显示 Cart.made。

要显示
3
exercise3.py
1class Cart:2    def __init__(self, load):3        self.load = load4 5a = Cart(3)6b = Cart(5)7c = Cart(2)
输出
点「运行」或按 Ctrl+Enter

这一课学会了

  • class 名字: 定义一种新类型;调用 名字(...) 就造出一个这种类型的对象。
  • __init__ 在造对象时运行,把一开始的属性设在 self 上。
  • self 是调用方法的那个对象:mill.switch_on() 运行 switch_on 时,self 就是 mill。
  • 方法是类里面的函数,可以读、可以改对象的属性。
  • self 上的属性属于一个对象;类属性所有对象共用。
  • 对象可以放进列表:循环、按某个属性求和、用 key 排序都行。