Python多继承MRO机制:C3算法原理与Mixin模式实战
1. 项目概述多继承与MRO的来龙去脉在Python的世界里面向对象编程OOP是其核心魅力之一。当你从单继承的舒适区走出来尝试使用多继承来构建更复杂的类关系时一个看似简单的问题就会浮出水面如果一个方法在多个父类中都存在Python究竟会调用哪一个这个问题背后就是方法解析顺序Method Resolution Order, MRO在发挥作用。MRO不是Python独有的概念但Python的实现方式——C3线性化算法——是其多继承模型稳健运行的关键。理解MRO不仅仅是记住一个规则更是理解Python如何组织类层次结构、如何避免“菱形继承”等经典难题以及如何设计出清晰、可维护的多继承代码的基础。无论你是正在为面试题“Python多继承的MRO顺序是什么”而苦恼还是在实际项目中遇到了因方法调用混乱而导致的诡异Bug深入理解MRO都将使你豁然开朗。2. 核心原理C3线性化算法深度拆解要彻底搞懂Python的MRO就必须深入到C3线性化算法。这个算法听起来高大上但其核心思想可以用一个更生活化的原则来概括“单调性”和“局部优先顺序”。2.1 为什么需要C3算法在C3算法之前Python 2.2及更早版本使用的是深度优先搜索DFS算法。这种算法在遇到经典的“菱形继承”问题时会产生问题。考虑这个结构class A: def method(self): print(“A”) class B(A): pass class C(A): def method(self): print(“C”) class D(B, C): pass如果使用深度优先从左到右D的查找顺序会是 D - B - A - C。当调用D().method()时会找到A的method并输出“A”。但这违背了我们的直觉既然C重写了A的方法并且C在D的继承列表中位于B之后我们可能期望调用C的方法。这就是DFS算法导致的非单调性问题——在更具体的子类C中重写的方法没有被更后代的类D优先使用。C3算法就是为了解决这类问题而生的它保证了MRO的单调性即如果一个类在MRO中排在另一个类之前那么这个顺序在其所有子类的MRO中都将保持不变。2.2 C3算法的计算规则C3算法的输入是一个类的继承列表如(B, C)输出是一个线性的类顺序列表。其计算过程遵循一个合并规则我们可以手动推导来加深理解。计算规则简述 对于某个类X其继承自父类B1, B2, ..., BN那么X的MRO是[X]与[父类们的MRO列表的合并]的合并结果。 合并操作的核心是从所有序列的第一个元素中选取一个不出现在任何其他序列尾部第一个元素之后的部分的类将其放入结果列表并从所有序列中移除该类。重复此过程直到所有序列为空。如果某一步找不到这样的类则说明继承关系存在矛盾Python会抛出TypeError。手动计算示例 让我们计算上面菱形继承例子中类D的MRO。 已知L(A) [A](L表示MRO列表)L(B) [B] merge(L(A), [A]) [B] merge([A], [A]) [B, A]L(C) [C] merge(L(A), [A]) [C] merge([A], [A]) [C, A]L(D) [D] merge(L(B), L(C), [B, C])现在计算L(D)L(D) [D] merge([B, A], [C, A], [B, C])看所有序列的第一个元素[B, A]的第一个是B[C, A]的第一个是C[B, C]的第一个是B。候选有B和C。检查B是否在其他序列的尾部非第一个位置。B在[B, C]的头部在[C, A]和[B, A]的尾部均未出现。所以选择B。将B放入结果并从所有序列中删除B删除后序列变为[A],[C, A],[C](注意[B, C]删除B后变为[C])现在第一个元素候选是A和C。检查A它出现在[C, A]的尾部第二个位置所以不能选。检查C它不在[A]或[C]的尾部所以选择C。将C放入结果删除C序列变为[A],[A],[]。现在只剩下A选择A。最终得到L(D) [D, B, C, A]。因此D().method()会按照 D - B - C - A 的顺序查找在C处找到并执行输出“C”。这符合我们的预期也保证了单调性。注意手动计算MRO是理解算法的最佳方式但在实际开发中我们永远使用类名.__mro__属性或类名.mro()方法来获取准确的MRO列表绝对不要依赖记忆或猜测。2.3super()函数与MRO的动态绑定理解了MRO的静态顺序下一个关键就是理解super()的动态行为。super()并不是简单地调用“父类”的方法而是按照当前类的MRO顺序调用下一个类的方法。class A: def method(self): print(“A”) class B(A): def method(self): print(“B start”) super().method() # 不是固定调用A.method而是调用MRO中B之后的下一个类的method print(“B end”) class C(A): def method(self): print(“C start”) super().method() print(“C end”) class D(B, C): def method(self): print(“D start”) super().method() print(“D end”) # D的MRO: [D, B, C, A, object] obj D() obj.method()输出会是D start B start C start A C end B end D end执行流程如下D.method开始打印“D start”然后super().method()根据D的MRO找到下一个类B调用B.method。B.method开始打印“B start”然后super().method()根据调用发起时实例obj所属类D的MRO找到B之后的下一个类C调用C.method。这里至关重要super()的查找依赖于最初调用发起时实例的MRO即D的MRO而不是B自己的MRO。C.method开始打印“C start”然后super().method()在D的MRO中找到C之后的下一个类A调用A.method。A.method打印“A”然后返回。控制权逐层返回依次打印“C end”、“B end”、“D end”。这种机制使得在多继承链中所有类通过super()协作成为可能每个类都只负责调用“下一个”类最终形成一个连贯的执行链。这是实现协作式多重继承如Mixin模式的基石。3. 实战应用利用MRO设计清晰的多继承结构理解了原理我们来看看如何在实战中用好MRO。多继承是一把双刃剑设计不当会导致代码像“意大利面条”一样难以理解。MRO为我们提供了设计和分析的罗盘。3.1 Mixin模式多继承的最佳实践Mixin是一种小型、功能单一的类它不是为了独立实例化而存在而是为了通过多继承将其功能“混合”到其他类中。Mixin类通常遵循一些约定不定义__init__方法或如果定义必须通过super().__init__()调用链上的其他__init__。通常不包含状态实例变量只提供方法。名字常以Mixin、Ability等结尾。示例实现日志和序列化功能的Mixinimport json class JSONSerializableMixin: 提供将实例转换为JSON字符串的能力 def to_json(self): # 注意这里简单实现实际中需要处理不可序列化对象 return json.dumps(self.__dict__, indent2) class LoggableMixin: 提供简单的日志记录能力 def log(self, message): print(f”[{self.__class__.__name__}] {message}”) class Person: def __init__(self, name, age): self.name name self.age age class Employee(LoggableMixin, JSONSerializableMixin, Person): def __init__(self, name, age, employee_id): # 必须调用super()来正确初始化所有父类 super().__init__(name, age) # 这会调用Person.__init__ self.employee_id employee_id def work(self): self.log(f”{self.name} is working.”) # 使用Mixin提供的方法 emp Employee(“Alice”, 30, “E123”) emp.work() # 输出: [Employee] Alice is working. print(emp.to_json()) # 输出JSON格式的实例字典MRO分析Employee.__mro__会是(Employee, LoggableMixin, JSONSerializableMixin, Person, object)。这保证了当调用emp.log时首先在Employee中查找未找到然后在LoggableMixin中找到。to_json同理。super().__init__的调用会沿着这个MRO链传递确保Person.__init__被正确调用。实操心得在设计Mixin时务必让它们保持“瘦身”。一个Mixin只做一件事。同时要仔细考虑Mixin在继承列表中的顺序因为它会影响MRO。通常将“功能提供型”Mixin放在“实体基类”之前如class MyClass(MixinA, MixinB, BaseClass)。3.2 处理初始化方法__init__的陷阱在多继承中__init__方法最容易出问题。如果多个父类都定义了__init__而子类没有妥善处理可能会导致某些父类未被初始化。错误示范class Base1: def __init__(self): self.attr1 “value1” print(“Base1 initialized”) class Base2: def __init__(self): self.attr2 “value2” print(“Base2 initialized”) class Derived(Base1, Base2): def __init__(self): # 错误只初始化了Base1 Base1.__init__(self) # Base2.__init__ 永远不会被调用 self.derived_attr “derived” d Derived() print(hasattr(d, ‘attr2’)) # 输出: False正确做法始终使用super()来保证初始化链的完整执行。class Base1: def __init__(self, **kwargs): super().__init__(**kwargs) # 关键 self.attr1 “value1” print(“Base1 initialized”) class Base2: def __init__(self, **kwargs): super().__init__(**kwargs) # 关键 self.attr2 “value2” print(“Base2 initialized”) class Derived(Base1, Base2): def __init__(self, **kwargs): super().__init__(**kwargs) # 这会沿着MRO链调用Base1.__init__, 然后是Base2.__init__ self.derived_attr “derived” print(“Derived initialized”) d Derived() print(d.attr1) # 输出: value1 print(d.attr2) # 输出: value2这里每个__init__都接受**kwargs并传递给super().__init__这是一种处理多继承中参数传递的通用模式。MRO(Derived, Base1, Base2, object)确保了初始化顺序为Derived - Base1 - Base2。3.3 菱形继承的经典场景与应对菱形继承是测试MRO理解程度的试金石。除了前面介绍的基本菱形还有更复杂的情况。场景覆盖中间类的方法class A: def process(self): print(“A.process”) class B(A): def process(self): print(“B.process start”) super().process() print(“B.process end”) class C(A): def process(self): print(“C.process start”) super().process() print(“C.process end”) class D(B, C): def process(self): print(“D.process start”) super().process() print(“D.process end”) d D() d.process()输出D.process start B.process start C.process start A.process C.process end B.process end D.process end这个输出完美展示了基于MRO的协作式调用。即使B和C都继承自A但通过super()它们并没有直接调用A而是按照D的MRO(D, B, C, A, object)顺序依次调用形成了一个完整的链条。这种模式在框架设计中非常有用例如中间件或处理器的管道模式。4. 高级话题与边界情况探讨掌握了基础和应用后我们来看看一些更深入或容易混淆的情况。4.1__mro__属性与mro()方法每个类都有__mro__这个元组属性和mro()这个类方法。它们本质返回相同的内容但mro()是一个方法可以被重写尽管极少需要而__mro__是类创建后由解释器设置的一个只读属性。在99.9%的情况下使用cls.__mro__查看即可。4.2 动态修改MRO的尝试与限制MRO是在类定义时即class语句执行时由C3算法计算确定并固化到__mro__属性中的。你无法在运行时修改一个已有类的MRO。以下操作都是无效的修改__bases__属性虽然可以修改类.__bases__但这会触发类重建可能导致不可预知的行为且修改后MRO会重新计算但原有实例的类引用可能不会更新极其危险不推荐。使用元类__new__干预你可以在元类的__new__方法中修改类的基类顺序从而影响最终生成的类的MRO。这是影响MRO的唯一“合法”时机但属于非常高级和罕见的用法通常用于框架开发。# 危险操作示例仅作了解切勿在生产环境使用 class A: pass class B: pass class C(A, B): pass print(C.__mro__) # (class ‘__main__.C’, class ‘__main__.A’, class ‘__main__.B’, class ‘object’) # 危险地修改 __bases__ C.__bases__ (B, A) print(C.__mro__) # MRO会改变但已创建的C实例可能出问题重要警告绝对不要试图在运行时修改__bases__来改变MRO。这破坏了Python对象模型的稳定性会导致难以调试的内存和行为错误。设计良好的代码应该依赖于清晰的静态类结构。4.3 非法继承结构与TypeError如果类的继承关系无法产生一个满足“单调性”的线性顺序Python在类定义时就会抛出TypeError。最常见的情况是继承顺序产生循环依赖。# 示例1直接的循环Python会阻止 class A(B): pass class B(A): pass # TypeError: Cannot create a consistent method resolution order (MRO) for bases A, B # 示例2更隐蔽的循环 class X: pass class Y(X): pass class Z(Y): pass class W(Z, X): pass # 这可能没问题因为MRO是 (W, Z, Y, X, object) # 但如果改成 class W(X, Z): pass # 可能产生矛盾因为X在Z之前但Z继承自YY继承自X顺序上X不能既在Z之前又在Z之后。 # 实际中Python的C3算法会检测并抛出TypeError。当你看到 “Cannot create a consistent method resolution order” 错误时就意味着你的类继承图不是一个“有向无环图DAG”或者基类顺序设置违反了局部优先顺序的一致性要求。解决方法是重新审视你的类设计通常意味着需要简化继承关系或者使用组合替代继承。4.4 使用inspect模块辅助分析inspect模块提供了getmro(cls)函数它返回和cls.__mro__相同的内容。在编写需要动态分析类结构的工具或框架时使用inspect.getmro()是更标准的方式。import inspect class A: pass class B(A): pass class C(B): pass print(inspect.getmro(C)) # (class ‘__main__.C’, class ‘__main__.B’, class ‘__main__.A’, class ‘object’)5. 调试技巧与常见问题排查在实际开发中当你觉得方法调用行为诡异时MRO往往是首要的怀疑对象。5.1 问题排查清单方法调用到了错误的父类第一步立即打印或查看相关类的MROprint(YourClass.__mro__)。第二步检查你的继承顺序是否符合设计预期。记住MRO顺序是深度优先、从左到右但受C3算法约束。第三步检查是否所有相关的__init__方法都正确使用了super().__init__()并传递了参数。super()调用链中断症状某个父类的方法没有被执行。检查点在继承链中间的某个类中是否忘记了调用super().method()这会导致链断裂。确保协作式方法中每个环节都传递super()调用。“TypeError: Cannot create a consistent method resolution order”原因继承关系存在循环或矛盾。解决绘制简单的类继承图检查是否存在循环。考虑使用“混入类Mixin”或“组合模式”替代复杂的多继承。5.2 一个复杂的调试案例假设我们有一个处理数据的管道由多个Mixin组成但最终输出不对。class InputMixin: def read(self): print(“Reading raw data”) self.data “raw” super().read() # 期望调用下一个处理环节 class ValidateMixin: def read(self): if hasattr(self, ‘data’): print(f”Validating {self.data}”) self.data “validated” super().read() class OutputMixin: def read(self): if hasattr(self, ‘data’): print(f”Outputting {self.data}”) super().read() # 可能调用到objectobject没有read方法 class DataProcessor(InputMixin, ValidateMixin, OutputMixin): def read(self): super().read() processor DataProcessor() try: processor.read() except AttributeError as e: print(f”Error: {e}”) print(“MRO:”, DataProcessor.__mro__)运行后可能会在OutputMixin的super().read()处抛出AttributeError: ‘super’ object has no attribute ‘read’。这是因为MRO的最后一个类是object它没有read方法。解决方案确保继承链的顶端有一个“锚点”基类它提供方法的默认实现甚至是空实现以确保super()调用链能安全结束。class DataPipelineBase: def read(self): 管道基类提供默认的空实现确保super()调用链有终点 pass # 或者 raise NotImplementedError 如果你希望子类必须重写 class InputMixin(DataPipelineBase): # Mixin也继承基类确保方法存在 def read(self): print(“Reading raw data”) self.data “raw” super().read() class ValidateMixin(DataPipelineBase): def read(self): if hasattr(self, ‘data’): print(f”Validating {self.data}”) self.data “validated” super().read() class OutputMixin(DataPipelineBase): def read(self): if hasattr(self, ‘data’): print(f”Outputting {self.data}”) super().read() # 现在会安全地调用到DataPipelineBase.read() class DataProcessor(InputMixin, ValidateMixin, OutputMixin): # 不需要显式继承DataPipelineBase因为Mixin已经继承了 def read(self): super().read() processor DataProcessor() processor.read()现在MRO会是(DataProcessor, InputMixin, ValidateMixin, OutputMixin, DataPipelineBase, object)调用链可以安全地在DataPipelineBase的read方法处终止。5.3 设计阶段的MRO检查在编写复杂的多继承类之前可以先用简单的代码验证MRO是否符合预期def check_mro(class_name, *base_classes): 快速检查拟定义类的MRO # 动态创建一个临时类 temp_class type(class_name, base_classes, {}) print(f”{class_name}({‘, ‘.join(b.__name__ for b in base_classes)}) MRO:“) for cls in temp_class.__mro__: print(f” {cls.__name__}”) print() # 在设计时验证 check_mro(‘MyClass’, MixinA, MixinB, BaseClass) check_mro(‘AnotherClass’, ClassX, ClassY, ClassZ)这个技巧可以帮助你在真正实现业务逻辑前确认继承结构是否会产生意外的解析顺序。

相关新闻

大模型Agent浏览器自动化:从RPA工程实践中提炼的5个反直觉设计

大模型Agent浏览器自动化:从RPA工程实践中提炼的5个反直觉设计

1. 项目概述:当Agent Loop遇上浏览器自动化最近在折腾一个基于大语言模型的自动化项目,核心是让一个Agent(智能体)去操作浏览器完成一些重复性任务。做着做着,我忽然有种强烈的既视感——这不就是RPA(机器人…

2026/10/13 17:46:16 阅读更多 →
突破复杂场景通信瓶颈:低功耗广域网(LPWAN)LoRa技术选型与工程实战

突破复杂场景通信瓶颈:低功耗广域网(LPWAN)LoRa技术选型与工程实战

在低功耗广域网(LPWAN)加速下沉的今天,智慧农业、地下管网、工业巡检及野外监测等场景对“长距离、低功耗、强穿透”的需求日益迫切。无线通信模块作为物联网终端的“神经末梢”,其选型的合理性直接决定了整个工程项目的稳定性与运…

2026/10/8 20:49:48 阅读更多 →
Excel合并单元格高效处理:4种取消合并与填充方案全解析

Excel合并单元格高效处理:4种取消合并与填充方案全解析

1. 项目概述:从“合并”到“高效”的思维跃迁在数据处理与分析这个行当里,Excel是绕不开的基石工具。我见过太多同事,包括一些经验丰富的分析师,在处理从业务部门或外部系统导出的报表时,面对那些为了“美观”而大量合…

2026/10/11 1:35:48 阅读更多 →

最新新闻

JDK1.8源码jar包制作与IDE关联调试实战指南

JDK1.8源码jar包制作与IDE关联调试实战指南

简介:JDK 1.8源码包完整收录了Java 8标准库的全部源代码,适合希望深入理解核心类库与并发模型的Java开发人员,也适合高校学生对照源码学习设计模式。借助这份包,可以在开发工具中直接打开并查看HashMap、ConcurrentHashMap、Strea…

2026/10/14 16:10:21 阅读更多 →
Android ViewPager2与BottomNavigationView双向联动实战避坑指南

Android ViewPager2与BottomNavigationView双向联动实战避坑指南

1. 这不是“套娃”,而是Android导航架构里最常被误用的联动模式你有没有遇到过这样的需求:一个页面顶部是TabLayout控制的横向滑动内容区,底部又有一排固定菜单按钮,点击底部菜单时,不仅切换底部选中状态,还…

2026/10/14 16:10:21 阅读更多 →
STK中文学习资料实战指南:从解压到跑通第一个场景

STK中文学习资料实战指南:从解压到跑通第一个场景

简介:这份STK中文学习资料包面向航天任务规划、遥感通信与导航领域的初学者及进阶用户,系统解决从软件入门到模块化应用的学习需求。包内共30个文件,以14份PDF教材为核心,辅以PPT讲义、GIF与BMP图示、DOC文档及XML、HTM等配置示例…

2026/10/14 16:10:21 阅读更多 →
MQTT在工业现场踩坑实录:4类场景慎用

MQTT在工业现场踩坑实录:4类场景慎用

把MQTT搬进工业现场这三年,我踩过的坑比调试时掉的头发还多。三年前给某电池厂做产线数据采集选型时,市面上主流的方案几乎都绕不开MQTT,当时我也被它的"轻量、异步、解耦"这几个词吸引,一股脑把几十台设备的工艺数据全…

2026/10/14 16:10:21 阅读更多 →
MQTT在工业现场的4个“不适合”场景与选型建议

MQTT在工业现场的4个“不适合”场景与选型建议

MQTT在工业现场用了3年,我总结的4个“不适合”场景1. 先说结论:MQTT在工业现场为什么会这么火,又为什么会被我拉进黑名单三年前上一家单位做产线数采改造,我作为通信方案负责人,一头扎进MQTT的怀里。那时候理由很充分&…

2026/10/14 16:10:21 阅读更多 →
The Nature of Code 极坐标与三角函数实战:用 sin/cos 在 p5.js 中绘制圆周、螺旋与任意形状

The Nature of Code 极坐标与三角函数实战:用 sin/cos 在 p5.js 中绘制圆周、螺旋与任意形状

教程文档示例工程 【免费下载链接】website-archive Archive of the Coding Train website (first version) 项目地址: https://gitcode.com/gh_mirrors/we/website-archive 点击查看 免费下载 本文以 website-archive 仓库中归档的《Trigonometry and Polar Coord…

2026/10/14 16:09:19 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个标题,我脑子里冒出来的第一个念头是:这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 22:01:27 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 7:53:34 阅读更多 →