Kodewick
第 27 课,共 31 课目录

继承

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

像素小镇有房子、商店和仓库。它们都是建筑:每座都有名字,有几扇要点亮的窗。有了继承,大家共有的东西只在 Building(建筑)类里写一次,别的类都从它出发。

main.py
1class Building:2    def __init__(self, name, windows):3        self.name = name4        self.windows = windows5 6    def describe(self):7        return f"{self.name},有 {self.windows} 扇窗"8 9class House(Building):10    pass11 12home = House("Ada 的家", 4)13print(home.describe())14print(home.windows)
输出
Ada 的家,有 4 扇窗
4

父类和子类

class House(Building): 让 House 成为 Building 的子类,Building 是它的父类(也叫基类)。括号里写的就是它继承的那个类。House 自己一行代码都没有(pass 的意思是「这里什么都不做」),但一座房子拥有建筑的一切:__init__、describe,以及它们设的属性。

调用 home.describe() 时,Python 先在 House 里找 describe。没有,就去父类 Building 里找,找到了。子类也可以加父类没有的东西:

main.py
1class Building:2    def __init__(self, name, windows):3        self.name = name4        self.windows = windows5 6class Shop(Building):7    def open_up(self):8        print(f"{self.name}开门了")9 10bakery = Shop("面包店", 6)11bakery.open_up()12town_hall = Building("镇公所", 12)13town_hall.open_up()
输出
面包店开门了
Traceback (most recent call last):
  File "main.py", line 13, in <module>
    town_hall.open_up()
AttributeError: 'Building' object has no attribute 'open_up'

发生了什么: 这种东西没有「open_up」这个方法。

商店能 open_up(开门),普通建筑不能:方法从父类传给子类,不会反过来。

覆盖方法

如果子类定义了一个和父类同名的方法,子类的对象就用子类的版本。这叫覆盖(override)。

先猜一猜

会显示什么?

1class Building:2    def lights_needed(self):3        return 24 5class Warehouse(Building):6    def lights_needed(self):7        return 108 9print(Building().lights_needed(), Warehouse().lights_needed())

super()

子类常常想做父类做的事,再多做一点。super() 给你父类的那个方法,直接调用就行,不用把父类的代码抄一遍。最常见的是在 __init__ 里:

main.py
1class Building:2    def __init__(self, name, windows):3        self.name = name4        self.windows = windows5 6    def describe(self):7        return f"{self.name},有 {self.windows} 扇窗"8 9class Shop(Building):10    def __init__(self, name, windows, goods):11        super().__init__(name, windows)12        self.goods = goods13 14    def describe(self):15        return super().describe() + f",卖{self.goods}"16 17bakery = Shop("面包店", 6, "面包")18print(bakery.describe())
输出
面包店,有 6 扇窗,卖面包

Shop.__init__ 让 Building.__init__ 去设 name 和 windows,自己再加上 goods。Shop.describe 拿到父类的那句话,在后面接上一段。点单步,看调用从子类走到父类再回来;从头到尾 self 都是同一家店。

如果子类的 __init__ 忘了调用 super().__init__,父类的 __init__ 就不会运行,它设的属性也就没有:

main.py
1class Building:2    def __init__(self, name):3        self.name = name4 5class House(Building):6    def __init__(self, name, owner):7        self.owner = owner8 9home = House("玫瑰小屋", "Ada")10print(home.owner)11print(home.name)
输出
Ada
Traceback (most recent call last):
  File "main.py", line 11, in <module>
    print(home.name)
AttributeError: 'House' object has no attribute 'name'

发生了什么: 这种东西没有「name」这个方法。

多态

不同的类可以用各自的方式回应同一个方法调用。只管调用方法的代码,不用知道手里是哪一种类。这个想法叫多态:

main.py
1class Building:2    def __init__(self, name):3        self.name = name4 5    def lights_needed(self):6        return 27 8class House(Building):9    pass10 11class Warehouse(Building):12    def lights_needed(self):13        return 1014 15class Mine(Building):16    def lights_needed(self):17        return 2518 19street = [House("玫瑰小屋"), Warehouse("货仓"), Mine("北矿")]20for b in street:21    print(b.name, b.lights_needed())22print(f"一共:{sum([b.lights_needed() for b in street])}")
输出
玫瑰小屋 2
货仓 10
北矿 25
一共:37

循环对每一样东西都一视同仁。明天再加一种新建筑,这个循环一个字都不用改。

isinstance 和 issubclass

isinstance(对象, 类) 问「这个对象是不是这个类」,子类也算:仓库是建筑。issubclass(子类, 父类) 对两个类问同样的问题。

main.py
1class Building:2    pass3 4class Warehouse(Building):5    pass6 7depot = Warehouse()8print(isinstance(depot, Warehouse), isinstance(depot, Building))9print(isinstance(Building(), Warehouse))10print(issubclass(Warehouse, Building), issubclass(Building, Warehouse))11print(type(depot) == Building)
输出
True True
False
True False
False

注意最后一行:type() 给的是确切的那个类,仓库的类型是 Warehouse,不是 Building。所以一般用 isinstance 来判断更好。它还能接收一个元组:isinstance(x, (int, float)),是哪种数字都算对。

先猜一猜

会显示什么?

1class Building:2    pass3 4class Shop(Building):5    pass6 7class Bakery(Shop):8    pass9 10print(isinstance(Bakery(), Building))

一切都继承自 object

没写括号的类,比如 class Lamp:,其实也有父类:Python 内置的 object。所有的类最后都通向它,int、str、list 也不例外。所以 Python 里的每个值都是 object:

main.py
1class Lamp:2    pass3 4print(issubclass(Lamp, object))5print(isinstance(5, object), isinstance("矿石", object))6print(issubclass(bool, int))7print(isinstance(True, int))
输出
True
True True
True
True

object 给每个类提供一些最基本的东西,比如一个什么都不做的 __init__:所以 Lamp 没写 __init__,Lamp() 也能用。最后两行有点意外:bool 是 int 的子类,所以 True 也算整数(它的表现和 1 一样)。

什么时候用继承

子类真的是父类的一种时,用继承:仓库是一种建筑,储蓄账户是一种银行账户。如果一样东西只是有另一样东西,就把它存成属性。街道不是灯,街道有灯:

main.py
1class Lamp:2    def __init__(self, brightness):3        self.brightness = brightness4 5class Street:6    def __init__(self, name):7        self.name = name8        self.lamps = []9 10    def add(self, lamp):11        self.lamps.append(lamp)12 13mill = Street("磨坊路")14mill.add(Lamp(3))15mill.add(Lamp(5))16print(f"{mill.name}有 {len(mill.lamps)} 盏灯")
输出
磨坊路有 2 盏灯

类的家族要小、要浅。一两层,比如 Building 和它的子类,很好懂;继承链一长,想弄清一个方法做了什么,就得在好几个类之间跳来跳去。

练一练

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

练习 1

吵闹的矿场

让 Mine 成为 Building 的子类,并覆盖 sound,让它返回 "叮当"。程序会显示下面两行。

要显示
安静
叮当
exercise1.py
1class Building:2    def sound(self):3        return "安静"4 5class Mine:6    pass7 8print(Building().sound())9print(Mine().sound())
输出
点「运行」或按 Ctrl+Enter
练习 2

储蓄账户

Savings.__init__ 应该让父类去设 owner 和 balance,自己再存 rate(利率)。用 super() 改好,让程序显示下面这一行。

要显示
Wick 5.0
exercise2.py
1class Account:2    def __init__(self, owner, balance):3        self.owner = owner4        self.balance = balance5 6class Savings(Account):7    def __init__(self, owner, balance, rate):8        self.rate = rate9 10s = Savings("Wick", 100, 0.05)11print(s.owner, s.balance * s.rate)
输出
点「运行」或按 Ctrl+Enter
练习 3

只算矿车

列表里混着 Cart(矿车)对象和别的值。用 isinstance,算出其中矿车的 load(载重)一共是多少。

要显示
12
exercise3.py
1class Cart:2    def __init__(self, load):3        self.load = load4 5items = [Cart(4), "绳子", Cart(7), 9, Cart(1)]6total = 07# 在这里写8print(total)
输出
点「运行」或按 Ctrl+Enter

这一课学会了

  • class 子类(父类): 造一个子类,它拥有父类所有的方法,还能加自己的。
  • 子类里同名的方法会覆盖父类的。
  • super().方法(...) 调用父类的版本;子类的 __init__ 一般先写 super().__init__(...)。
  • 多态:不同的类用各自的方式回应同一个方法调用,一个循环就能处理它们全部。
  • isinstance 和 issubclass 把父类也算进去;type() 只给确切的那个类。
  • 所有的类都继承自 object。「是一种」用继承;「有一个」用属性。