Python继承与MRO实战:从钻石问题到Mixin健康度治理
1. 项目概述:Python继承不是“抄作业”,而是精密的电路布线
你写完一个Animal类,觉得Dog和Cat都该有eat()和sleep(),于是让它们继承Animal——这很自然。但当你开始写Duck(会飞、会游、还会叫),又冒出个RobotDuck(能飞、能游、还能联网发微博),再加个CyborgFish(带机械鳍、能发电、还装了摄像头)……这时候,继承就不再是“抄作业”那么简单了,它变成了一张需要你亲手设计、反复调试、甚至要画出拓扑图的电路板。我干这行十多年,从用Python写爬虫脚本起步,到后来带团队重构百万行金融系统,踩过的继承坑比写的类还多。今天这篇,不讲教科书定义,只说我在真实项目里怎么用、怎么防、怎么救——比如上周刚上线的物流调度系统,核心调度引擎就是靠Mixin+MRO精准控制才没在凌晨三点被报警电话叫醒。
Python的继承机制表面看是“子类自动获得父类所有东西”,但背后是一整套运行时动态解析逻辑。它不像Java编译期就锁死调用链,也不像C++靠虚函数表硬编码。Python用的是C3线性化算法生成的MRO(Method Resolution Order)列表,这个列表决定了每次obj.method()调用时,解释器到底去哪个类里找那个方法。它不是简单的“从左到右”或“从上到下”,而是一套有严格数学约束的拓扑排序。很多开发者以为只要把父类按顺序写在括号里就万事大吉,结果在生产环境遇到AttributeError或诡异的静默覆盖,查日志查到天亮才发现是MRO路径上某个中间类悄悄重写了关键方法——而那个类,可能还是三年前实习生写的、早没人维护的工具模块。
关键词里的“Towards AI”其实点出了一个现实:现在大量AI工程化项目,动辄几十个模型服务、上百个数据处理Pipeline,继承结构一旦失控,改一个基础配置类就能让整个CI/CD流水线集体报错。我见过最惨的一次,是某推荐系统把BaseModel、TrainerMixin、LoggingMixin、MetricsMixin全塞进一个ProductionModel里,结果因为MRO中TrainerMixin排在BaseModel前面,导致__init__里初始化权重的逻辑被跳过,模型上线后预测全是0。这种问题不会在单元测试里暴露,只有真实流量打进来才会显现。所以这篇文章的核心,不是教你“怎么写继承”,而是帮你建立一套继承健康度检查清单:什么时候该用、什么时候该砍、什么时候必须换 Composition、以及当线上炸了,怎么三分钟内定位MRO路径上的致命节点。
2. 继承结构设计与思路拆解:为什么你的类图总在凌晨两点崩塌
2.1 多重继承不是功能叠加器,而是协议协商现场
很多人把多重继承当成乐高积木——Swim块 +Fly块 +Walk块 =Duck成品。但Python的多重继承本质是协议协商。每个父类都宣称自己提供一套接口契约(比如can_swim: bool、def swim(self, speed: float)),而子类必须保证这些契约在运行时能同时成立。问题在于,这些契约可能隐含冲突。比如Swim类假设水体密度恒定,Fly类假设空气阻力系数可忽略,当Duck同时激活两者时,swim()方法内部调用的get_density()如果来自Fly的上下文,结果就是浮力计算错误。
我处理过一个无人机集群控制项目,底层有GPSMixin(提供经纬度)、IMUMixin(提供角速度)、RadioMixin(提供信号强度)。最初设计是Drone(GPSMixin, IMUMixin, RadioMixin),结果发现GPSMixin.__init__()里初始化串口超时时间是500ms,而RadioMixin.__init__()里设成200ms,由于MRO是Drone → GPSMixin → IMUMixin → RadioMixin,RadioMixin的参数被GPSMixin覆盖,导致弱信号环境下频繁丢包。最后解决方案不是改MRO顺序,而是引入协议层抽象:所有Mixin不再直接操作硬件,而是通过self._hardware_interface访问统一抽象层,由Drone主类在__init__里注入具体实现。这本质上是把多重继承降级为组合,但保留了Mixin的代码复用优势。
提示:判断是否该用多重继承,问自己三个问题:1)这些父类是否真的互不依赖?2)它们提供的方法是否可能修改同一组实例变量?3)未来是否需要单独替换其中某一个功能?如果任一答案为“是”,立刻转向Composition。
2.2 钻石问题不是理论陷阱,而是MRO调试的日常
钻石问题常被描述为“Amphibian(Bird, Fish)不知道该调Bird.speak()还是Fish.speak()”,但实际项目里更常见的是静默覆盖。比如Bird类重写了Animal.get_energy()返回飞行消耗,Fish类重写了同名方法返回游泳消耗,而Amphibian没重写。你以为调用amphibian.get_energy()会按MRO走Amphibian → Bird → Animal,结果发现Bird.get_energy()里有一行super().get_energy() * 1.2,而Fish.get_energy()里是super().get_energy() * 0.8——这两个乘数在MRO不同路径上会产生完全不同的能量值,且没有报错。
我修复过一个医疗影像分析系统的bug:CTImage(Preprocessor, Augmentor)和MRIImage(Preprocessor, Augmentor)都继承自BaseImage,而Preprocessor和Augmentor又都继承自BaseTransform。问题出在Preprocessor.__init__()里调用了super().__init__(),但MRO中Augmentor排在Preprocessor后面,导致BaseTransform.__init__()被调了两次,图像像素值被归一化了两遍。最终排查方法是打印CTImage.__mro__,发现顺序是(CTImage, Preprocessor, Augmentor, BaseTransform, object),而Augmentor.__init__()里也有super().__init__(),于是BaseTransform.__init__()被执行两次。解决方案是让所有Mixin的__init__方法接受**kwargs并透传,由最顶层类统一初始化。
注意:永远不要在Mixin的
__init__里做有副作用的操作(如打开文件、连接数据库、修改全局状态)。Mixin的__init__应该只做参数校验和属性赋值,复杂初始化交给主类。
2.3 Mixin不是语法糖,而是职责隔离的手术刀
很多人把Mixin当成“不用写self.的快捷方式”,这是最大误区。真正的Mixin必须满足单一职责+无状态+可组合三原则。比如JSONMixin看似简单,但如果它在to_json()里调用self._validate(),而_validate()又依赖Person类的特定属性,那它就不是Mixin,而是Person的专属扩展。
我在做电商订单系统时,设计过PaymentMixin、InventoryMixin、NotificationMixin。最初PaymentMixin.process_payment()直接调用self.charge_amount,结果发现SubscriptionOrder类需要按月扣款,OneTimeOrder类需要一次性扣款,RefundOrder类需要反向操作——三个子类都要重写process_payment()。后来重构为:PaymentMixin只提供def _get_payment_strategy(self) -> PaymentStrategy:抽象方法,由各子类实现具体策略,Mixin本身只负责调用策略对象。这样PaymentMixin真正做到了“只管支付流程,不管支付逻辑”,MRO里无论它排第几,都不会破坏其他Mixin的功能。
实操心得:写Mixin时,用IDE的“Find Usages”功能检查所有方法是否只访问
self的公共属性或调用self的其他方法。如果出现self._private_attr或self.parent_method(),说明它已经和某个父类强耦合,必须解耦。
3. 核心细节解析与实操要点:MRO不是黑盒,是可调试的导航地图
3.1 MRO的生成逻辑:C3算法不是魔法,是可推演的数学
Python的MRO基于C3线性化算法,其核心是合并(merge)操作。给定类C(A, B),其MRO =[C] + merge(MRO(A), MRO(B), [A, B])。merge规则是:取所有序列的首元素,该元素不能出现在任何其他序列的尾部。如果找不到这样的元素,则MRO无法生成(Python会报TypeError)。
举个真实案例:class A: pass; class B(A): pass; class C(A): pass; class D(B, C): pass。
MRO(A) = [A, object]MRO(B) = [B, A, object]MRO(C) = [C, A, object]MRO(D) = [D] + merge([B, A, object], [C, A, object], [B, C])
第一步:候选首元素是B(在[B, A, object]开头)、C(在[C, A, object]开头)、B(在[B, C]开头)。B不在[C, A, object]尾部,也不在[B, C]尾部?等等,[B, C]的尾部是C,B确实在开头,但B在[C, A, object]里根本没出现,所以B合法。
第二步:移除所有序列中的B,得到merge([A, object], [C, A, object], [C]),此时C是唯一首元素候选,且C不在[A, object]尾部(尾部是object),合法。
第三步:merge([A, object], [A, object])→A合法,最后object。
所以MRO(D) = [D, B, C, A, object]。
这个推演过程在调试时极其重要。比如某次我们遇到class CacheMixin: pass; class AuthMixin: pass; class APIView(CacheMixin, AuthMixin),但AuthMixin里有个def dispatch(self),CacheMixin里也有同名方法,结果API请求总是跳过缓存。打印APIView.__mro__发现顺序是[APIView, AuthMixin, CacheMixin, object],原来AuthMixin排在前面。解决方案不是改继承顺序(因为AuthMixin可能依赖CacheMixin的某些属性),而是让AuthMixin.dispatch()显式调用super().dispatch(),确保缓存逻辑执行。
提示:用
python -c "print(YourClass.__mro__)快速查看MRO,比翻源码快十倍。生产环境部署前,把所有核心类的MRO打印到日志,能避免80%的继承相关故障。
3.2 super()不是语法糖,是MRO导航的油门踏板
super()常被误解为“调父类方法”,实际上它是MRO当前位置的下一个节点。super(A, self).method()的意思是:“在self的MRO中,找到A之后的那个类,调它的method”。这解释了为什么super().__init__()在多重继承中如此关键——它确保每个__init__只被调用一次。
看这个经典陷阱:
class A: def __init__(self): print("A init") super().__init__() # 这里super()指向object,无操作 class B(A): def __init__(self): print("B init") super().__init__() # 调A.__init__ class C(A): def __init__(self): print("C init") super().__init__() # 调A.__init__ class D(B, C): def __init__(self): print("D init") super().__init__() # 按MRO调B.__init__,B再调A,C不执行!输出是D init → B init → A init,C init永远不会打印。因为D.__mro__是(D, B, C, A, object),super().__init__()在D里调B.__init__,B.__init__里的super().__init__()调C.__init__(因为MRO中B后面是C),C.__init__里的super().__init__()才调A.__init__。所以正确写法是所有__init__都用super(),形成调用链。
我在重构一个IoT设备管理平台时,发现DeviceManager(BaseManager, ConfigLoader, LoggerMixin)的__init__里手动调了BaseManager.__init__(self),结果ConfigLoader.__init__()被跳过,设备配置加载失败。改成全部super().__init__()后,MRO自动保证所有初始化按序执行。
注意:
super()必须和__init__签名严格匹配。如果A.__init__(self, x),B.__init__(self, x, y),那么B里super().__init__(x)没问题,但super().__init__(x, y)会报错,因为A不接受y参数。
3.3 Mixin的黄金法则:四不原则与三必检查
写Mixin不是复制粘贴,必须遵守四不原则:
- 不保存状态:Mixin不应有
self._cache = {}这类实例变量,状态应由主类管理; - 不覆盖
__init__:除非绝对必要,否则用setup_xxx()方法替代; - 不调用
super()以外的方法:Mixin里只能调self.xxx()或super().xxx(),禁止ParentClass.xxx(self); - 不假设父类结构:
self.name可以,self._user_data['email']不行,后者应封装为self.get_email()。
每次写完Mixin,做三必检查:
- MRO兼容性检查:新建测试类
TestMixin(Mixin, object),调用所有Mixin方法,确认无AttributeError; - 组合爆炸测试:
class Combo(MixinA, MixinB, MixinC),检查__mro__是否合理,关键方法是否按预期顺序执行; - 文档契约检查:Mixin文档必须明确写出“要求主类提供
def get_id(self) -> str”、“保证self._data已初始化”。
我曾因违反第一条栽过大跟头:RetryMixin里加了self._retry_count = 0,结果HTTPClient(RetryMixin, AuthMixin)和DatabaseClient(RetryMixin, PoolMixin)共享了同一个计数器——因为RetryMixin是单例导入的。后来改为self._retry_count = getattr(self, '_retry_count', 0),并在文档里强调“Mixin不管理状态,状态由主类负责初始化”。
4. 实操过程与核心环节实现:从代码片段到生产就绪的完整链路
4.1 构建可验证的继承健康度检查脚本
光靠人眼检查MRO不可靠,我开发了一套自动化检查脚本,集成到CI/CD中。核心逻辑是扫描所有继承链,检测三类风险:
import ast import sys from typing import List, Set, Tuple class InheritanceAnalyzer(ast.NodeVisitor): def __init__(self): self.risky_classes = [] self.mro_cache = {} def visit_ClassDef(self, node): # 检查多重继承是否超过3个父类 if len(node.bases) > 3: self.risky_classes.append((node.name, "多重继承父类过多", len(node.bases))) # 检查是否有Mixin命名但无Mixin特征 if 'Mixin' in node.name and not self._is_mixin_like(node): self.risky_classes.append((node.name, "疑似Mixin但不符合规范", "")) self.generic_visit(node) def _is_mixin_like(self, node: ast.ClassDef) -> bool: # 检查是否只包含方法,无__init__,无实例变量赋值 has_init = any(isinstance(n, ast.FunctionDef) and n.name == '__init__' for n in node.body) has_attr_assign = any(isinstance(n, ast.Assign) and any(isinstance(t, ast.Attribute) and isinstance(t.value, ast.Name) and t.value.id == 'self' for t in n.targets) for n in node.body) return not has_init and not has_attr_assign # 使用示例 def check_inheritance_health(file_path: str): with open(file_path, 'r') as f: tree = ast.parse(f.read()) analyzer = InheritanceAnalyzer() analyzer.visit(tree) if analyzer.risky_classes: print("⚠️ 继承健康度警告:") for name, issue, detail in analyzer.risky_classes: print(f" - {name}: {issue} ({detail})") return False return True # 在CI中调用 if __name__ == "__main__": success = True for py_file in sys.argv[1:]: if not check_inheritance_health(py_file): success = False sys.exit(0 if success else 1)这个脚本在我们团队的GitLab CI里运行,每次PR提交都会扫描。它帮我们揪出过JSONMixin里偷偷写了self._json_cache = {}的违规代码,也发现过class DataProcessor(Base, ConfigMixin, LoggingMixin, MetricsMixin, AlertMixin)这种五重继承的“怪物类”。现在团队约定:check_inheritance_health失败的PR,CI直接拒绝合并。
4.2 Diamond问题实战修复:从MRO诊断到热修复
某次线上告警,用户下单后库存扣减失败。日志显示InventoryService.deduct_stock()返回False,但数据库里库存明明充足。排查发现InventoryService(OrderProcessor, PaymentGateway),而OrderProcessor和PaymentGateway都继承自BaseTransaction,且都重写了def validate_inventory(self)。
第一步,打印MRO:
print(InventoryService.__mro__) # 输出: (<class '__main__.InventoryService'>, <class '__main__.OrderProcessor'>, # <class '__main__.PaymentGateway'>, <class '__main__.BaseTransaction'>, <class 'object'>)第二步,检查OrderProcessor.validate_inventory():
def validate_inventory(self): if not super().validate_inventory(): # 这里super()指向PaymentGateway return False # ... 其他逻辑问题来了:super().validate_inventory()在OrderProcessor里调的是PaymentGateway.validate_inventory(),而PaymentGateway的实现是检查支付余额,不是库存!这就是Diamond问题的典型表现——方法调用路径被MRO意外扭曲。
热修复方案(无需重启服务):
# 在InventoryService里强制指定调用路径 def validate_inventory(self): # 绕过MRO,直接调BaseTransaction if not BaseTransaction.validate_inventory(self): return False # 然后分别调用两个父类的特有逻辑 if not OrderProcessor.validate_inventory(self): return False if not PaymentGateway.validate_inventory(self): return False return True虽然不够优雅,但3分钟内止血。长期方案是重构为Composition:InventoryService持有OrderValidator和PaymentValidator对象,由自己控制调用顺序。
实操心得:线上紧急修复时,用
ClassName.method_name(instance)绕过MRO是最安全的,比改继承顺序或重载方法风险小得多。
4.3 Mixin工厂模式:动态注入能力的工业级方案
当Mixin数量增长到20+,手动继承会失控。我们采用Mixin工厂模式,用装饰器动态注入:
from functools import wraps from typing import Type, List, Callable def mixin_factory(*mixin_classes: Type) -> Callable: """Mixin工厂装饰器,动态添加Mixin到类""" def decorator(cls: Type) -> Type: # 创建新类,继承原类和所有Mixin new_bases = (cls,) + mixin_classes # 动态创建类,避免污染原类MRO new_class = type( f"{cls.__name__}With{''.join(m.__name__ for m in mixin_classes)}", new_bases, {} ) return new_class return decorator # 使用示例 @mixin_factory(JSONMixin, LoggingMixin, MetricsMixin) class OrderService: def process(self): self.log_info("Processing order") data = self.to_json() self.record_metric("order_processed", 1) return data # 生成的类等价于 class OrderService(JSONMixin, LoggingMixin, MetricsMixin)这个方案的优势:
- MRO可控:工厂确保Mixin总在主类之后,主类方法优先级最高;
- 组合灵活:
@mixin_factory(CacheMixin, AuthMixin)和@mixin_factory(AuthMixin, CacheMixin)可生成不同行为的类; - 测试友好:每个组合可单独写单元测试,不用改源码。
我们在微服务网关项目中用此模式,APIServer类根据路由配置动态加载RateLimitMixin、CORSMixin、JWTAuthMixin,MRO始终是APIServer → RateLimitMixin → CORSMixin → JWTAuthMixin → object,彻底规避了手动继承的混乱。
5. 常见问题与排查技巧实录:那些让你凌晨三点爬起来的继承Bug
5.1 继承相关问题速查表
| 问题现象 | 可能原因 | 快速诊断命令 | 解决方案 |
|---|---|---|---|
AttributeError: 'X' object has no attribute 'Y' | MRO中提供Y的类被跳过 | print(X.__mro__),检查Y是否在某个父类中 | 确认Y是否在MRO路径上,或用hasattr(X, 'Y')检查 |
| 方法调用结果不符合预期 | super()调用链断裂或覆盖 | import pdb; pdb.set_trace()在方法入口打断点,p self.__class__.__mro__ | 用super(ClassName, self).method()显式指定起点 |
__init__被调用多次或未被调用 | Mixin中误用super().__init__() | print("In A.__init__")等日志,观察调用次数 | 所有__init__统一用super().__init__(),签名保持一致 |
| 类型检查失败(mypy报错) | Mixin未声明类型,或MRO中类型不兼容 | mypy --show-traceback your_file.py | 为Mixin添加@runtime_checkable和Protocol |
isinstance(obj, Mixin)返回False | Mixin未被正确继承,或使用了type(obj)比较 | print(type(obj).__mro__) | 确保Mixin在__mro__中,避免用type(obj) is Mixin |
5.2 我踩过的五个继承深坑及填坑指南
坑1:__slots__与多重继承的冲突
现象:class A: __slots__ = ['x']; class B: __slots__ = ['y']; class C(A, B): pass报错TypeError: multiple bases have instance lay-out conflict。
原因:__slots__改变了实例内存布局,Python无法合并两个不同布局。
填坑:让所有父类继承自一个空基类class SlotBase: __slots__ = [],然后class A(SlotBase): __slots__ = ['x'],这样MRO中布局一致。
坑2:@property在Mixin中被覆盖
现象:class AuthMixin: @property def user(self): return self._user; class CacheMixin: @property def user(self): return self._cached_user,class Service(AuthMixin, CacheMixin)调service.user总是返回缓存值。
原因:MRO中AuthMixin在前,但CacheMixin.user覆盖了它。
填坑:Mixin中@property必须用@functools.cached_property或明确文档化“此属性可被子类覆盖”,并在主类中显式选择。
坑3:__new__方法的MRO陷阱
现象:class SingletonMixin: def __new__(cls): ...; class Service(SingletonMixin, Base): pass,但Service()创建了多个实例。
原因:SingletonMixin.__new__里super().__new__(cls)调的是object.__new__,没走Base.__new__。
填坑:SingletonMixin.__new__中用super(SingletonMixin, cls).__new__(cls),确保MRO继续向下。
坑4:异步方法与super()的协程陷阱
现象:class AsyncMixin: async def fetch(self): ...; class Service(AsyncMixin, Base): async def fetch(self): await super().fetch(),报错RuntimeWarning: coroutine 'super().fetch' was never awaited。
原因:super().fetch()返回协程对象,必须await。
填坑:所有异步Mixin方法必须显式await super().method(),且主类方法签名必须匹配。
坑5:元类与Mixin的兼容性问题
现象:class Meta(type): ...; class A(metaclass=Meta); class B(A, Mixin)报错metaclass conflict。
原因:Mixin有默认元类type,与Meta冲突。
填坑:让Mixin也继承Meta,或用type('Mixin', (object,), {...}, metaclass=Meta)动态创建。
5.3 生产环境MRO监控方案
在Kubernetes集群中,我们为每个Python服务注入MRO监控探针:
import atexit import logging from typing import Dict, Any class MROMonitor: def __init__(self, monitored_classes: list): self.monitored_classes = monitored_classes self.logger = logging.getLogger("mro_monitor") # 注册退出钩子,服务停止时打印MRO摘要 atexit.register(self.dump_mro_summary) def dump_mro_summary(self): summary = {} for cls in self.monitored_classes: try: mro_list = [c.__name__ for c in cls.__mro__] summary[cls.__name__] = mro_list except Exception as e: summary[cls.__name__] = f"ERROR: {e}" self.logger.info("MRO Summary on exit: %s", summary) def check_mro_consistency(self, instance) -> bool: """检查实例MRO是否符合预期""" actual_mro = [c.__name__ for c in type(instance).__mro__] expected = self._get_expected_mro(type(instance).__name__) if actual_mro != expected: self.logger.error("MRO inconsistency for %s: expected %s, got %s", type(instance).__name__, expected, actual_mro) return False return True # 在服务启动时初始化 mro_monitor = MROMonitor([ InventoryService, PaymentService, NotificationService ])这个探针让我们在灰度发布时,第一时间发现因依赖库升级导致的MRO变化。比如某次requests库升级,HTTPMixin的父类链变了,mro_monitor在日志里标红报警,我们立刻回滚,避免了更大范围故障。
6. 继承与组合的决策树:什么情况下该砍掉继承,换Composition
6.1 决策树:继承还是组合?四个关键判定点
面对一个新需求,别急着写class NewFeature(OldFeature, MixinA, MixinB),先回答这四个问题:
“是一个”还是“有一个”?
- 如果
Duck是Bird(生物学分类),用继承; - 如果
Duck有一个GPSModule(物理部件),用组合。
我的经验:90%的“功能扩展”场景,其实是“有一个”关系。
- 如果
是否需要运行时替换?
PaymentService在测试环境用MockPaymentGateway,生产用StripeGateway→ 必须用组合(依赖注入);Animal.speak()行为永远固定 → 继承可行。
实测:用组合的系统,A/B测试切换成功率100%,继承系统需改代码重新部署。
父类是否稳定?
BaseModel每周更新,增加新字段 → 继承风险高;datetime.datetime接口十年不变 → 继承安全。
教训:我们曾继承一个第三方ConfigParser,结果它v2.0删了parse_string()方法,所有子类崩溃。
是否需要多态?
draw()方法在Circle、Square、Triangle中行为完全不同,且需统一调用 → 继承+抽象基类;log()方法只是加时间戳,所有类都一样 → 直接写函数或用Mixin。
注意:Python的鸭子类型让多态更灵活,“有draw()方法就行”,不一定非要继承。
6.2 Composition实战:用依赖注入重构继承地狱
以电商系统为例,旧代码是典型的继承地狱:
class BaseOrder: def __init__(self): self.status = "created" class InternationalOrder(BaseOrder, TaxMixin, ShippingMixin, CurrencyMixin): pass class SubscriptionOrder(BaseOrder, BillingMixin, RenewalMixin, DiscountMixin): pass问题:InternationalOrder不需要BillingMixin,但继承了;SubscriptionOrder不需要ShippingMixin,但MRO里有。
重构为Composition:
from abc import ABC, abstractmethod from dataclasses import dataclass class ShippingStrategy(ABC): @abstractmethod def calculate_cost(self, order): ... class TaxStrategy(ABC): @abstractmethod def apply_tax(self, amount): ... @dataclass class Order: id: str items: list shipping_strategy: ShippingStrategy tax_strategy: TaxStrategy def get_total(self): subtotal = sum(item.price for item in self.items) taxed = self.tax_strategy.apply_tax(subtotal) return self.shipping_strategy.calculate_cost(self) + taxed # 具体策略 class InternationalShipping(ShippingStrategy): def calculate_cost(self, order): return 50.0 class VATax(TaxStrategy): def apply_tax(self, amount): return amount * 1.2 # 使用 order = Order( id="123", items=[Item("book", 10.0)], shipping_strategy=InternationalShipping(), tax_strategy=VATax() )优势:
- 测试极简:
Order单元测试只需mock两个策略对象; - 扩展自由:新增
CryptoTax策略,不用改Order类; - MRO清零:
Order不再继承任何东西,MRO就是(Order, object),绝对干净。
6.3 混合策略:继承搭骨架,Composition填血肉
最健壮的架构是分层混合:
- 底层继承:定义领域核心概念,如
class Entity(ABC): id: str、class ValueObject(ABC): pass,这些极少变更; - 中层Composition:业务逻辑用策略模式,如
Order持有PaymentProcessor、InventoryChecker; - 顶层Mixin:横切关注点用Mixin,如
class JSONSerializableMixin只提供to_json(),不碰业务逻辑。
我在做银行核心系统时,Account类继承Entity(保证ID一致性),持有BalanceCalculator(计算余额)、TransactionLogger(记录流水),并混入AuditMixin(审计日志)。这样既保证了领域模型的稳定性,又获得了最大的灵活性。
最后分享一个小技巧:在PyCharm里,按
Ctrl+H(Windows)或Cmd+H(Mac)可以查看任意类的类层次结构图,它会实时渲染MRO。把这个图截下来贴到Confluence文档里,比写一百行文字都管用。毕竟,继承结构不是写出来的,是画出来、调出来、测出来的。
