- 引言
面向对象编程(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 委托 / 依赖注入"]
- 类与实例
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。它只是约定俗成的名字,但强烈建议不要改成别的。
- 属性与方法
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
- 继承与方法重写
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["D"] --> B["B"]
B --> C["C"]
C --> A["A"]
A --> O["object"]
多重继承虽然强大,但容易带来复杂度,实际项目中应谨慎使用。
- 多态
多态指同一接口在不同类型上有不同实现。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["make_sound(obj)"] --> Q{"obj 是否有 speak 方法"}
Q -->|"是"| R["调用 obj.speak()"]
Q -->|"否"| X["抛出 AttributeError"]
R --> D1["Duck:嘎嘎"]
R --> D2["Robot:滴滴"]
R --> D3["Cat:喵喵"]
如果需要显式约束接口,可以使用抽象基类:
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
- 组合优于继承的实践
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 关系且父类设计稳定时才使用继承。
- 总结
- 类定义模板,实例承载具体数据,
self指向当前实例; - 实例属性各自独立,类属性共享,
property让属性访问更可控; - 继承用于表达 is-a 关系,重写配合
super()复用父类逻辑; - 多态让调用方只依赖接口,Python 的鸭子类型让这一点更自然;
- 组合通过委托复用功能,通常比继承更灵活、更易维护。
在实际项目中,先想清楚对象之间是"是什么"还是"有什么",往往就能做出正确的结构选择。
结构清晰、示例贴切的 Python OOP 入门到进阶指南,尤其适合已会写类但常滥用继承的开发者,用组合与依赖注入思路重构代码结构。