目录

1.引言

2.多继承:一把威力巨大的双刃剑

 3.MRO:让多继承有条不絮的“家谱”

4.多态:Python 的“隐形接口”

5.公有、私有与保护:访问控制的哲学

6.综合案例:插件式架构

7.结语


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 把继承图转化为单调线性列表,满足:

  1. 子类永远在父类前;

  2. 父类按声明顺序出现;

  3. 保证祖先只出现一次。

示例:

       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 的 privateprotected 关键字,但通过命名约定与语言特性实现同等效果。

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 世界里既写出脚本般的灵动,也撑得起企业级的厚重。 

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐