系列文章

Python 从入门到精通

第 14 / 36 篇

从环境安装和基础语法出发,逐步学习工程实践、自动化、数据处理与 Web 开发。

  1. 01
    Python 怎么装、怎么用
  2. 02
    变量、数字和字符串
  3. 03
    列表:把一组数据放在一起
  4. 04
    元组、集合和字典
  5. 05
    条件判断:让程序做选择
  6. 06
    循环:重复的事交给程序
  7. 07
    函数:把代码整理成可复用的块
  8. 08
    模块与包:拆分你的程序
  9. 09
    输入、输出与字符串格式化
  10. 10
    文件读写:保存程序的数据
  11. 11
    异常处理:程序出错时怎么办
  12. 12
    基础阶段练习:命令行记账本
  13. 13
    类和对象:面向对象入门
  14. 14
    继承、组合与特殊方法正在阅读
  15. 15
    迭代器与生成器
  16. 16
    列表推导式与生成器表达式
  17. 17
    装饰器:给函数增加能力
  18. 18
    上下文管理器与 with
  19. 19
    类型标注与 dataclass
  20. 20
    正则表达式:从文本中找规律
  21. 21
    日期、时间与时区
  22. 22
    日志与调试
  23. 23
    虚拟环境与依赖管理
  24. 24
    测试:让修改不再提心吊胆
  25. 25
    网络请求:用 Python 调用 API
  26. 26
    网页解析与合规采集
  27. 27
    操作 Excel、CSV 与批量文件
  28. 28
    SQLite:给程序加一个数据库
  29. 29
    数据分析入门:NumPy 与 Pandas
  30. 30
    画图:把数据变得直观
  31. 31
    Flask 入门:做一个小网站
  32. 32
    异步编程:同时处理多项任务
  33. 33
    线程、进程与并发选择
  34. 34
    性能分析与优化
  35. 35
    项目结构、配置与发布
  36. 36
    综合项目:从需求到上线

查看整个系列 →

上一篇学了类和对象的基础。但面向对象不止”把数据和函数捆一起”——现实里东西是有层次的:狗是动物,猫也是动物,它们共享”会吃会睡”,又各有各的本事。继承就是表达这种”是某种东西”的关系。这一篇讲继承、组合,以及几个神奇的魔术方法。

继承:子类复用父类

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

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

    def sleep(self):
        print(f"{self.name} 在睡觉")

class Dog(Animal):          # Dog 继承 Animal
    def bark(self):
        print(f"{self.name}:汪汪!")

class Cat(Animal):          # Cat 继承 Animal
    def meow(self):
        print(f"{self.name}:喵~")

d = Dog("旺财")
c = Cat("咪咪")
d.eat()      # 旺财 在吃饭(继承来的)
d.bark()     # 旺财:汪汪!(自己的)
c.sleep()    # 咪咪 在睡觉(继承来的)
c.meow()     # 咪咪:喵~(自己的)

子类(Dog)自动拥有父类(Animal)的所有方法和属性,还能加自己的。这就是代码复用:公共逻辑写一次,子类各加各的。

覆写:子类改父类的方法

子类可以重新定义同名方法,覆盖父类行为:

class Dog(Animal):
    def eat(self):                    # 覆写
        print(f"{self.name} 狼吞虎咽地吃")

class Bird(Animal):
    def eat(self):
        print(f"{self.name} 啄着吃")

但更常见的是:想保留父类的逻辑,再加点自己的。用 super() 调父类的方法:

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)      # 调父类 __init__,把 name 交给它处理
        self.breed = breed

    def eat(self):
        super().eat()               # 先执行父类的 eat
        print("然后舔了舔嘴")

super() 就是”父类”,别自己复制父类代码,用 super 调用才是正确姿势。

多重继承(了解即可)

Python 支持一个类继承多个父类,但慎用——两个父类有同名方法时,调用顺序容易绕晕:

class Flyable:
    def fly(self):
        print("飞起来了")

class Swimmable:
    def swim(self):
        print("游起来了")

class Duck(Flyable, Swimmable):
    pass

d = Duck()
d.fly()    # 飞起来了
d.swim()   # 游起来了

简单的”会飞、会游”这种组合用多重继承还行,复杂了建议用组合(下面讲)。

组合:优先于继承

很多时候,”有一个”比”是一个”更自然。比如汽车有发动机,而不是汽车继承发动机:

class Engine:
    def start(self):
        print("发动机启动了")

class Car:
    def __init__(self):
        self.engine = Engine()      # 组合:Car 里有一个 Engine

    def start(self):
        self.engine.start()         # 委托给它
        print("汽车可以走了")

car = Car()
car.start()
# 发动机启动了
# 汽车可以走了

业界有句话叫“组合优于继承”(composition over inheritance):能用组合表达的关系,别硬套继承。继承是”is-a”(狗是一种动物),组合是”has-a”(车有一个发动机)。分不清时,优先组合。

魔术方法:让对象更”原生”

双下划线开头结尾的方法统称魔术方法,Python 在特定场景自动调用它们。上一篇的 __str__ 是其一,再认识几个常用的:

class Score:
    def __init__(self, value):
        self.value = value

    def __str__(self):
        return f"分数({self.value})"

    def __len__(self):               # len(obj) 时调用
        return self.value

    def __eq__(self, other):         # == 时调用
        return self.value == other.value

    def __lt__(self, other):         # < 时调用(排序用)
        return self.value < other.value

    def __add__(self, other):        # + 时调用
        return Score(self.value + other.value)

a = Score(85)
b = Score(90)
print(a == b)        # False(__eq__)
print(a < b)         # True(__lt__)
print(a + b)         # 分数(175)(__add__)
print(len(a))        # 85(__len__)
print(sorted([b, a]))  # [分数(85), 分数(90)](__lt__ 支持排序)

有了这些,自定义类就能像内置类型一样用 +==len()、排序。这就是”让类融入 Python 生态”。

property:把方法当属性用

有时候想”读取时计算一下”,又不想暴露方法。用 @property

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    @property
    def area(self):                # 像属性一样访问,不用加括号
        return self.width * self.height

    @property
    def is_square(self):
        return self.width == self.height

r = Rectangle(4, 5)
print(r.area)          # 20(不是 r.area()!)
print(r.is_square)     # False

好处:外部用法不变(r.area),内部可以从”存好的值”改成”现算的”,调用方无感知。

新手坑

坑 1:继承层级太深。三层以内还好,五层以上改一个父类全局地震。浅一点,多用组合。

坑 2:忘记 super().__init__。子类定义了 __init__ 又不调 super 的,父类属性全没了,访问报 AttributeError。

坑 3:把 __str__ 写成 __repr__ 或反过来。两个都定义最省心:__repr__ 给开发者看(调试),__str__ 给用户看(print)。只写一个就写 __str__

小结

  • 继承:class Dog(Animal),子类复用父类代码,可加新方法
  • 覆写 + super():保留父类逻辑再扩展,别复制父类代码
  • 组合优于继承:is-a 用继承,has-a 用组合
  • 魔术方法:__eq__/__lt__/__add__/__len__ 让类支持运算符和内置函数
  • @property 把计算方法变成属性,调用方无感知

练习

  1. 定义 Shape 父类(name 属性、describe 方法),CircleRectangle 继承它并各自实现 area()
  2. Money 类实现 __add____eq__,让两个金额对象能相加、比较
  3. 思考:鸟和飞机都能飞。用继承怎么写?用组合怎么写?哪种在”给鸟加个下蛋方法”时更不容易出问题?