Python 面向对象进阶四重奏——多继承、MRO、多态与访问控制
目录
1.引言
Python 之所以能够在脚本语言和大型工程之间无缝切换,核心原因之一是其对面向对象范式完整而灵活的支持:多重继承、MRO、动态多态以及极简而有效的访问控制。本文尝试用 3000 字把四个主题串成一条可落地的知识链,使读者在“写得出”与“写得好”之间完成跃迁
2.多继承:一把威力巨大的双刃剑
2.1 语法与直观效果
Python 允许多继承,语法简洁:
class Flyable:
def fly(self):
print("I can fly")
class Swimmable:
def swim(self):
print("I can swim")
class Duck(Flyable, Swimmable):
pass
d = Duck()
d.fly(); d.swim()
Duck 同时获得两个父类的全部方法,仿佛魔法。但魔法背后需要成本:
-
如果 Flyable 与 Swimmable 拥有同名方法,谁会最终胜出?
-
如果它们还有共同祖先,祖先的构造器会运行几次?
2.2 菱形继承与“两条爸爸”问题
把 Flyable、Swimmable 的公共父类 Animal 加进来:
class Animal:
def __init__(self):
print("Animal init")
class Flyable(Animal):
def __init__(self):
super().__init__()
print("Flyable init")
class Swimmable(Animal):
def __init__(self):
super().__init__()
print("Swimmable init")
class Duck(Flyable, Swimmable): # 菱形继承
def __init__(self):
super().__init__()
print("Duck init")
运行 Duck() 的输出:
Animal init → Flyable init → Swimmable init → Duck init
看起来 Animal 只初始化了一次,这得益于 super() 与 MRO 的协同。
2.3 构造器协同:super() 的正确姿势
常见错误:显式调用父类 Animal.__init__(self),会导致 Animal 被多次执行。正确做法是:
-
所有参与多重继承的类都必须使用
super(); -
参数签名最好保持一致,或使用
**kwargs透传; -
遵循“初始化即结束”原则:一旦某个类完成初始化,就立即返回。
3.MRO:让多继承有条不絮的“家谱”
3.1 经典类与新式类的历史包袱
Python 2.2 以前只有经典类 (classic class),MRO 采用深度优先、从左到右,易导致菱形问题。新式类 (new-style) 统一继承自 object,从 2.3 起引入 C3 线性化。Python 3 全部为新式类。
3.2 C3 线性化算法图解
C3 把继承图转化为单调线性列表,满足:
-
子类永远在父类前;
-
父类按声明顺序出现;
-
保证祖先只出现一次。
示例:
O
/ \
A B
/ \ / \
C D E
\ | /
F
F.mro 会给出:F → C → D → A → E → B → O → object
3.3 动态查看与调试 MRO
print(Duck.__mro__)
# (<class '__main__.Duck'>, <class '__main__.Flyable'>,
# <class '__main__.Swimmable'>, <class '__main__.Animal'>,
# <class 'object'>)
调试技巧:
-
inspect.getmro()对老代码兼容; -
利用
__mro__快速定位方法解析顺序; -
出现 AttributeError 时,打印 MRO 确认是否走错分支。
4.多态:Python 的“隐形接口”
4.1 鸭子类型与 EAFP
Python 不依赖类型声明,只要对象“像鸭子一样嘎嘎叫”,就可以当鸭子处理:
def draw(widget):
widget.paint() # 不关心 widget 的具体类型
EAFP (Easier to Ask Forgiveness than Permission) 让多态无需前置检查:
try:
widget.paint()
except AttributeError:
fallback()
4.2 抽象基类 (ABC):给鸭子加上“脚环”
鸭子类型虽好,但大型项目需要显式接口。abc 模块提供了抽象基类:
from abc import ABC, abstractmethod
class Drawable(ABC):
@abstractmethod
def paint(self): ...
class Button(Drawable):
def paint(self):
print("Draw Button")
Button 必须实现 paint,否则实例化时报错。ABC 还能注册虚拟子类:
Drawable.register(int) # int 现在也被视为 Drawable
4.3 泛函数与模式匹配的新玩法
从 Python 3.4 起 functools.singledispatch 提供了基于类型的泛函数:
from functools import singldispatch
@singledispatch
def area(shape):
raise NotImplementedError
@area.register
def _(rect: Rectangle):
return rect.w * rect.h
@area.register
def _(c: Circle):
return 3.14 * c.r ** 2
Python 3.10 引入结构化模式匹配,为多态再添利器:
match shape:
case Rectangle(w, h):
return w * h
case Circle(r):
return 3.14 * r ** 2
5.公有、私有与保护:访问控制的哲学
Python 的封装哲学是“大家都是成年人”,因此没有 Java 的 private、protected 关键字,但通过命名约定与语言特性实现同等效果。
5.1 名称改写 (Name Mangling)
在属性前加双下划线,Python 会自动把 __name 改为 _ClassName__name:
class BankAccount:
def __init__(self):
self.__balance = 0
def deposit(self, amt):
self.__balance += amt
acc = BankAccount()
print(acc._BankAccount__balance) # 强行访问
优点:防止子类意外覆盖;缺点:调试时略显晦涩。
5.2 单下划线约定
单前划线 _var 表示“内部实现”,IDE 会提示但语法层面仍可访问。PEP 8 建议:
-
模块级变量
_helper不导出; -
类属性
_cache仅供内部使用; -
子类可自由访问,但需自担风险。
5.3 property 与描述符:更精细的封装
使用 @property 将属性访问转化为方法调用,实现只读、校验、惰性加载:
class Celsius:
def __init__(self, temp=0):
self._temp = temp
@property
def temperature(self):
return self._temp
@temperature.setter
def temperature(self, value):
if value < -273.15:
raise ValueError("Below absolute zero")
self._temp = value
描述符协议 (__get__, __set__, __delete__) 进一步抽象出可复用字段,如 ORM 字段类型。
6.综合案例:插件式架构
需求:开发一个可插拔的日志系统,支持输出到终端、文件、邮件,且第三方可继续扩展。
6.1 定义接口
from abc import ABC, abstractmethod
class Logger(ABC):
@abstractmethod
def log(self, msg): ...
class TerminalLogger(Logger):
def log(self, msg):
print("[TERM]", msg)
class FileLogger(Logger):
def __init__(self, path):
self.path = path
def log(self, msg):
with open(self.path, "a") as f:
f.write(msg + "\n")
6.2 多继承组合功能
假设需要同时支持“带时间戳”与“带颜色”两种可选能力:
class TimestampMixin:
def log(self, msg):
from datetime import datetime
super().log(f"{datetime.now():%F %T} {msg}")
class ColorMixin:
COLOR = "\033[32m"
RESET = "\033[0m"
def log(self, msg):
super().log(f"{self.COLOR}{msg}{self.RESET}")
6.3 组装插件
class FancyTerminalLogger(ColorMixin, TimestampMixin, TerminalLogger):
pass
fl = FancyTerminalLogger()
fl.log("Hello Plugin") # 绿色时间戳日志
6.4 动态发现插件
使用 importlib.metadata 扫描 entry points,第三方只需在 setup.cfg 注册:
[options.entry_points]
mylogger.plugins
email = email_logger:EmailLogger
主程序:
from importlib.metadata import entry_points
for ep in entry_points(group="mylogger.plugins"):
logger_cls = ep.load()
registry[ep.name] = logger_cls
至此,多继承、MRO、多态、访问控制全部派上用场:
-
多继承完成 mixin 组合;
-
MRO 保证 TimestampMixin → ColorMixin → TerminalLogger 的顺序;
-
多态让主程序只依赖 Logger 接口;
-
双下划线保护内部 registry,单下划线缓存插件配置。
7.结语
多继承提供了强大的组合能力,MRO 让这种能力井然有序;多态则把“会做什么”与“是什么”解耦,使系统可弹性生长;公有、私有与保护机制在“开放”与“封闭”之间给予开发者恰到好处的安全感。掌握这四件套,便能在 Python 世界里既写出脚本般的灵动,也撑得起企业级的厚重。
更多推荐
所有评论(0)