Python 面向对象编程:类、继承、多态与组合

文章来源声明: 原文作者:vipxieliang; 来源站点:掘金; 原文链接:https://juejin.cn/post/7701269657706315811; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

结构清晰、示例贴切的 Python OOP 入门到进阶指南,尤其适合已会写类但常滥用继承的开发者,用组合与依赖注入思路重构代码结构。

目录 --
  1. 引言

面向对象编程(OOP)是 Python 中最常用的编程范式之一。它把数据和对数据的操作封装在一起,让代码更贴近现实世界的建模方式,也更容易复用和扩展。

本文围绕五个核心主题展开:类与实例、属性与方法、继承与方法重写、多态,以及组合优于继承的实践。读完后你应当能够独立设计出结构清晰、易于维护的类体系。

下面这张图展示了本文的知识脉络,方便你建立整体认知:

flowchart TD
  A["Python 面向对象编程"] --> B["类与实例"]
  A --> C["属性与方法"]
  A --> D["继承与方法重写"]
  A --> E["多态"]
  A --> F["组合优于继承"]
  B --> B1["class 定义 / 实例化 / self"]
  C --> C1["实例属性 / 类属性 / property"]
  D --> D1["重写 / super() / MRO"]
  E --> E1["鸭子类型 / 抽象基类"]
  F --> F1["has-a 委托 / 依赖注入"]

  1. 类与实例

2.1 定义一个类

在 Python 中,使用 class 关键字定义类,类名通常采用大驼峰命名法。

class Dog:
    """一个简单的狗类"""

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def bark(self):
        return f"{self.name} 在汪汪叫"

2.2 创建实例

类只是模板,实例才是真正占用内存、携带具体数据的对象。

d1 = Dog("旺财", 3)
d2 = Dog("小黑", 5)

print(d1.bark())  # 旺财 在汪汪叫
print(d2.bark())  # 小黑 在汪汪叫
print(d1 is d2)   # False,两个独立实例

每个实例都有自己独立的属性空间,d1.name 和 d2.name 互不影响。

2.3 self 是什么

self 代表当前实例本身。调用 d1.bark() 时,Python 实际上执行的是 Dog.bark(d1),self 就是 d1。它只是约定俗成的名字,但强烈建议不要改成别的。

  1. 属性与方法

3.1 实例属性与类属性

  • 实例属性:在 __init__ 中通过 self.xxx 定义,每个实例各有一份。
  • 类属性:直接定义在类体中,所有实例共享。
class Dog:
    species = "犬科"          # 类属性,所有实例共享

    def __init__(self, name):
        self.name = name      # 实例属性,每个实例独立

d1 = Dog("旺财")
d2 = Dog("小黑")

print(d1.species, d2.species)  # 犬科 犬科
Dog.species = "Canidae"
print(d1.species)              # Canidae,类属性被修改后所有实例可见

注意:通过实例给同名属性赋值,会在实例上新建一个属性,从而"遮蔽"类属性。

d1.species = "自定义"
print(d1.species)  # 自定义
print(d2.species)  # Canidae

3.2 实例方法、类方法与静态方法

class Circle:
    pi = 3.14159

    def __init__(self, radius):
        self.radius = radius

    def area(self):                 # 实例方法:第一个参数是 self
        return self.pi * self.radius ** 2

    @classmethod
    def from_diameter(cls, diameter):  # 类方法:第一个参数是 cls
        return cls(diameter / 2)

    @staticmethod
    def is_valid_radius(r):         # 静态方法:没有 self/cls
        return r > 0

  • 实例方法用于操作具体对象的数据;
  • 类方法常用于提供替代构造器;
  • 静态方法只是放在类命名空间里的普通函数。

3.3 属性封装与 property

Python 没有真正的私有成员,约定用下划线表示非公开:_name 表示受保护,__name 会触发名称改写(name mangling)。

需要校验或计算属性时,用 @property 更优雅:

class Person:
    def __init__(self, age):
        self._age = age

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, value):
        if value < 0:
            raise ValueError("年龄不能为负数")
        self._age = value

p = Person(20)
p.age = 25
print(p.age)   # 25
# p.age = -1   # 抛出 ValueError

  1. 继承与方法重写

4.1 基本继承

子类在类名后的括号中写明父类,即可继承父类的属性和方法。

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

    def speak(self):
        return "..."

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

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

上面的继承关系可以用下图表示:

classDiagram
  class Animal {
    +name
    +speak()
  }
  class Cat {
    +speak()
  }
  class Dog {
    +speak()
  }
  class Puppy {
    +age
    +speak()
  }
  Animal <|-- Cat
  Animal <|-- Dog
  Dog <|-- Puppy

4.2 方法重写与 super()

子类定义与父类同名的方法即为重写。若仍想复用父类逻辑,用 super() 调用。

class Puppy(Dog):
    def __init__(self, name, age):
        super().__init__(name)   # 复用父类初始化
        self.age = age

    def speak(self):
        base = super().speak()   # 复用父类行为
        return f"{base}(奶声奶气)"

p = Puppy("豆豆", 1)
print(p.speak())  # 豆豆:汪汪(奶声奶气)

4.3 多重继承与 MRO

Python 支持多重继承,方法查找顺序由 MRO(方法解析顺序)决定,遵循 C3 线性化算法。

class A:
    def hello(self):
        return "A"

class B(A):
    def hello(self):
        return "B -> " + super().hello()

class C(A):
    def hello(self):
        return "C -> " + super().hello()

class D(B, C):
    pass

print(D().hello())      # B -> C -> A
print([c.__name__ for c in D.__mro__])  # ['D', 'B', 'C', 'A', 'object']

D 的方法查找顺序(MRO)如下图所示,super() 会沿着这条链依次向上调用:

flowchart LR
  D[&#34;D&#34;] --> B[&#34;B&#34;]
  B --> C[&#34;C&#34;]
  C --> A[&#34;A&#34;]
  A --> O[&#34;object&#34;]

多重继承虽然强大,但容易带来复杂度,实际项目中应谨慎使用。

  1. 多态

多态指同一接口在不同类型上有不同实现。Python 是动态类型语言,天然支持"鸭子类型":只要对象有需要的方法,就能被统一处理,不要求继承关系。

class Duck:
    def speak(self):
        return "嘎嘎"

class Robot:
    def speak(self):
        return "滴滴"

def make_sound(obj):
    print(obj.speak())

for item in [Duck(), Robot(), Cat("咪咪")]:
    make_sound(item)

输出:

嘎嘎
滴滴
咪咪:喵喵

make_sound 不关心对象的具体类型,只关心它是否有 speak 方法。这正是多态带来的解耦能力。

多态的调用过程可以概括为下图:

flowchart TD
  S[&#34;make_sound(obj)&#34;] --> Q{&#34;obj 是否有 speak 方法&#34;}
  Q -->|&#34;是&#34;| R[&#34;调用 obj.speak()&#34;]
  Q -->|&#34;否&#34;| X[&#34;抛出 AttributeError&#34;]
  R --> D1[&#34;Duck:嘎嘎&#34;]
  R --> D2[&#34;Robot:滴滴&#34;]
  R --> D3[&#34;Cat:喵喵&#34;]

如果需要显式约束接口,可以使用抽象基类:

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        ...

class Square(Shape):
    def __init__(self, side):
        self.side = side

    def area(self):
        return self.side ** 2

# Shape()  # TypeError:抽象类不能实例化
print(Square(3).area())  # 9

  1. 组合优于继承的实践

6.1 继承的问题

继承表达的是"is-a"关系。当子类只是为了复用代码而继承时,往往会出现:

  • 继承层次过深,理解成本高;
  • 父类改动波及所有子类(脆弱基类问题);
  • 多重继承导致 MRO 复杂、行为难以预测。

6.2 用组合表达"has-a"

组合是把一个对象作为另一个对象的属性,通过委托完成功能复用,表达"has-a"关系。

class Engine:
    def start(self):
        return "引擎启动"

class Wheel:
    def roll(self):
        return "车轮滚动"

class Car:
    def __init__(self):
        self.engine = Engine()
        self.wheels = [Wheel() for _ in range(4)]

    def start(self):
        return self.engine.start()

    def run(self):
        return ",".join(w.roll() for w in self.wheels)

car = Car()
print(car.start())  # 引擎启动
print(car.run())    # 车轮滚动,车轮滚动,车轮滚动,车轮滚动

Car 不需要继承 Engine,只需持有一个引擎对象。替换引擎实现时,Car 的代码几乎不用改。

组合关系如下图所示,Car 通过持有 Engine 与 Wheel 完成功能复用:

classDiagram
  class Car {
    +Engine engine
    +Wheel[] wheels
    +start()
    +run()
  }
  class Engine {
    +start()
  }
  class Wheel {
    +roll()
  }
  Car *-- Engine
  Car *-- Wheel

6.3 一个更贴近实战的例子

假设要记录日志,可以按输出目标拆分策略,而不是为每种组合写一个子类。

class ConsoleWriter:
    def write(self, msg):
        print(f"[console] {msg}")

class FileWriter:
    def __init__(self, path):
        self.path = path

    def write(self, msg):
        with open(self.path, "a", encoding="utf-8") as f:
            f.write(msg + "\n")

class Logger:
    def __init__(self, writer):
        self.writer = writer

    def log(self, msg):
        self.writer.write(msg)

Logger(ConsoleWriter()).log("hello")
Logger(FileWriter("app.log")).log("hello")

如果改用继承,就需要 ConsoleLogger、FileLogger,再叠加格式化、过滤等维度时子类数量会爆炸。组合让每个维度独立变化。

6.4 选择建议

场景推荐
子类确实是父类的一种(is-a)继承
只是想复用某个类的功能(has-a)组合
需要运行时切换行为组合 + 依赖注入
需要定义统一接口抽象基类 / 协议

一句话总结:优先使用组合,只有在明确的 is-a 关系且父类设计稳定时才使用继承。

  1. 总结

  • 类定义模板,实例承载具体数据,self 指向当前实例;
  • 实例属性各自独立,类属性共享,property 让属性访问更可控;
  • 继承用于表达 is-a 关系,重写配合 super() 复用父类逻辑;
  • 多态让调用方只依赖接口,Python 的鸭子类型让这一点更自然;
  • 组合通过委托复用功能,通常比继承更灵活、更易维护。

在实际项目中,先想清楚对象之间是"是什么"还是"有什么",往往就能做出正确的结构选择。