🐸 青蛙编程网
首页 / Python / 面向对象 / 3. 继承与多态

继承与多态

这是什么:继承 = 子类在括号里认一个父类,自动获得父类全部属性和方法。

为什么用:狗会叫、猫会叫、仓鼠也要会叫,难道每个类都从零写一遍?不用继承,代码全是复制粘贴,改一处得改一片。

如何用:本章先从 class Dog(Animal) 写起,再讲同名方法重写和 super() 怎么接着父类的活干。

继承语法:class 子类(父类)

class Animal:
    def __init__(self, name):
        self.name = name

    def eat(self):
        print(f"{self.name}在吃饭")


class Dog(Animal):        # 括号里写父类
    def speak(self):
        print(f"{self.name}:汪汪!")


d = Dog("旺财")           # __init__ 也是继承来的
d.speak()                 # 自己的方法
d.eat()                   # 父类的方法,白拿
旺财:汪汪!
旺财在吃饭
写法 含义
class Dog(Animal) Dog 是子类,Animal 是父类;子类自动拥有父类全部属性和方法
d.speak()、d.eat() 查找顺序:先找自己,再找父类,都找不到才报错

方法重写与 super()

父类的方法不合口味?子类定义同名方法即可重写(覆盖);想保留父类功能再加工,用 super() 调用父类版本。

class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        print(f"{self.name}发出了声音")


class Cat(Animal):
    def speak(self):                 # 重写:覆盖父类版本
        print(f"{self.name}:喵~")


class Kitten(Animal):
    def __init__(self, name):
        super().__init__(name)       # 借父类之手完成初始化
        self.cute = True             # 再添点自己的


Cat("咪咪").speak()
k = Kitten("奶球")
k.speak()                # 没重写,原样用父类的
print(k.name, k.cute)
咪咪:喵~
奶球发出了声音
奶球 True

多态:同一句话,各家各腔

不同子类重写同一个方法后,同一行调用,不同对象有不同反应——这就是多态,对象版的"看人下菜碟"。

class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        print(f"{self.name}发出了声音")


class Dog(Animal):
    def speak(self):
        print(f"{self.name}:汪汪!")


class Cat(Animal):
    def speak(self):
        print(f"{self.name}:喵~")


zoo = [Dog("旺财"), Cat("咪咪"), Dog("小黑")]
for a in zoo:
    a.speak()        # 同一句调用,猫猫狗狗各叫各的
旺财:汪汪!
咪咪:喵~
小黑:汪汪!

记忆口诀:括号里是爹,家底全继承;同名就翻新,super 找爹帮忙;一句 speak,各家唱各腔。

❓ 常见问题

Q1:子类重写了 __init__,却报 AttributeError: 'Kitten' object has no attribute 'name'?

原因:子类一旦定义了自己的 __init__,父类的就不再自动执行;新 __init__ 里没给 self.name 赋值,后面取 name 自然扑空。 修复三步:① 检查子类是不是自己写了 __init__;② 在里面补一句 super().__init__(name)(参数按父类的要求传);③ 再写子类新增的属性。 举一反三:重写 __init__ 的铁律——先 super 借爹之手干完老活,再干自己的新活。

Q2:写了 super.speak() 报 AttributeError: type object 'super' has no attribute 'speak'?

原因:super 后面忘了括号。super() 是"喊爹"这个动作,返回父类的代理人;光写 super 只是提到这个名字,Python 跑去一个叫 super 的类型身上找 speak,当然没有。 修复三步:① 定位报错行;② 给 super 补上括号:super().speak();③ 重跑,确认输出里多了父类的内容。 举一反三:见到 type object 'xxx' has no attribute,先怀疑"该加括号的地方没加"——super() 和实例化 Dog() 都是括号家族的。

Q3:子类没重写的方法,改了父类会影响子类吗?

原因:会,而且立刻生效。子类白拿的方法并没有抄一份存自己家里,而是"用的时候顺着血缘去父类拿"——父类一改,所有没重写的子类跟着变。 修复三步:① 改父类公共方法前,先数一数有几个子类在用;② 确认新行为对它们都成立再动手;③ 某个子类需要不同行为,就在它自己类里重写。 举一反三:这正是继承的威力也是风险——"改一处、全家更新",项目越大,父类越要谨慎修改。

Q4:什么时候该用继承,什么时候不该?

原因:判断口诀是"是不是"(is-a):狗是一种动物,通顺,继承;发动机是汽车的一部分(has-a),不通顺,别继承——把发动机做成汽车的属性装进去更合理。硬凑的继承会让代码越长越歪。 修复三步:① 造句"A 是一种 B";② 通顺就写 class A(B);③ 不通顺就改用"组合"——在 B 里放一个 A 类型的属性。 举一反三:ElectricCar(Car) 通顺;Engine(Car) 就很怪——发动机不是一种汽车,是汽车的一个零件。

Q5:循环里一句 a.speak(),Python 怎么知道该叫哪一声?

原因:这就是多态。列表里混装着 Dog、Cat 各家对象,调用时 Python 按每个对象自己的类找 speak:先找自己有没有重写,有就用自家的,没有才去父类——同一行代码,各家各腔。 修复三步:① 记住查找顺序"先自己、再父类";② 想让某家不同腔,就在它类里重写同名方法;③ 想整体换新玩法,只改父类,没重写的全家自动跟上。 举一反三:以后做"不同会员等级不同折扣""不同格式导出文件",都是"一个循环 + 各自重写"的多态套路,能省掉一大坨 if。

⚠️ 新手翻车现场

翻车一:重写 __init__ 却忘了喊爹

class Animal:
    def __init__(self, name):
        self.name = name


class Kitten(Animal):
    def __init__(self, name):        # 自己写了一套
        self.cute = True             # 忘了 super().__init__


k = Kitten("奶球")
print(k.name)
AttributeError: 'Kitten' object has no attribute 'name'

逐行分析:错在第 7~8 行——子类自定义 __init__ 后,父类那份不再自动执行,self.name 从没被赋值,第 8 行只挂了 cute。修复:在 self.cute = True 前加一句 super().__init__(name)。

翻车二:super 忘了括号

class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        print("发出了声音")


class Cat(Animal):
    def speak(self):
        super.speak()                # 忘了括号


Cat("咪咪").speak()
AttributeError: type object 'super' has no attribute 'speak'

逐行分析:错在第 11 行——super 后面没括号,Python 把它当成一个普通类型去找 speak 属性,当然找不到。补上括号变成 super().speak(),才是"喊爹执行"。

翻车三:重写时偷改参数清单

class Animal:
    def __init__(self, name):
        self.name = name


class Dog(Animal):
    def speak(self, word):           # 比父类多要一个参数
        print(f"{self.name}:{word}")


d = Dog("旺财")
d.speak()                            # 一个参数都没给
TypeError: Dog.speak() missing 1 required positional argument: 'word'

逐行分析:病根在第 7 行——重写 speak 时偷偷加了个 word 参数;报错在第 12 行引爆——调用端还按老习惯不传参,供需对不上。重写方法时参数清单尽量和父类保持一致,要加就得所有调用处一起改。

📦 完整小示例

图形面积计算器:父类定骨架,子类各算各的公式,一个循环全部搞定。

class Shape:
    def __init__(self, name):
        self.name = name

    def area(self):
        print(f"{self.name}还不会算面积")


class Square(Shape):
    def __init__(self, side):
        super().__init__("正方形")
        self.side = side

    def area(self):
        print(f"{self.name}面积:{self.side * self.side}")


class Circle(Shape):
    def __init__(self, r):
        super().__init__("圆形")
        self.r = r

    def area(self):
        print(f"{self.name}面积:{3.14 * self.r * self.r}")


shapes = [Square(4), Circle(2), Square(10)]
for s in shapes:
    s.area()

你会看到:

正方形面积:16
圆形面积:12.56
正方形面积:100

逐段中文精讲

  • 第 1~6 行(父类兜底):Shape 收个名字,给了个"默认版" area——谁没重写就用它兜底,好比 Animal 里的"发出了声音"。
  • 第 8~14 行(正方形):super().__init__("正方形") 借父类之手把名字挂好,再挂自己的 side;area 重写成"边长 × 边长"。
  • 第 16~22 行(圆形):同一个两步走——super 初始化、重写 area 换成 3.14 × r × r。
  • 第 24~25 行(多态上线):列表混装两种形状,一句 s.area() 各算各的——Python 按对象自己的类找 area,正方形走正方形的公式,圆形走圆形的,循环体一行没变。

跑通后做个小实验:新建 Triangle(Shape),__init__ 收底和高,重写 area 打印"底 × 高 ÷ 2",把它塞进列表——循环一个字不改,新形状自动加入算面积大军。

🎯 什么时候用它

  • 游戏单位:基础 Unit 定好血量、移动,Warrior、Mage 继承后只重写攻击方式;新职业上线就是新增一个子类的事。
  • 支付方式:Payment 父类定好"pay"这个动作,微信支付、支付宝各自重写细节;收银台一句 p.pay() 通吃全家。
  • 界面控件:Widget 定好"绘制、响应点击"的骨架,按钮、输入框继承后各自微调;界面上百个控件共享一套底座。

📝 本章小结

  • class 子类(父类) 白拿父类全部家底;方法查找顺序:先自己、再父类
  • 子类定义同名方法即重写覆盖;super().方法() 借父类原版之力
  • 重写 __init__ 记得先 super().__init__(...),否则父类属性全体失踪
  • 多态:循环里同一句调用,不同对象各干各的,省掉成堆 if
  • 记忆锚点:继承是"拼爹开局",super 是"喊爹帮忙",重写是"青出于蓝",多态是"同一句口令、各家各动作"。

动手改

  1. 给 Animal 加 describe 方法,打印"我是 + 名字",让 Dog、Cat 一行不写就拥有它
  2. 新建 Hamster(Animal),重写 speak 打印"吱吱!",把它加进 zoo 循环
  3. 把 Cat 的 speak 改成:先打印"喵~",再调 super().speak(),看输出变成什么样

下一章:魔术方法,解锁双下划线的隐藏能力 🐸

📥 下载本章代码

ZIP 内含本章全部代码文件与说明,解压即用,与教程内容同步生成。