c3 linearization详解
MRO
MRO 全称方法解析顺序(Method Resolution Order),在多重继承和多继承存在的时候,寻找属性及方法的顺序。
深度优先(DFS)与广度优先(BFS)
python2 所用的 mro 就是深度优先的算法,但是深度优先针对菱形继承会有问题,如图:
D(D) -->B(B)
D(D) -->C(C)
B(B) -->A(A)
C(C) -->A(A)
DFS: A->B->D->C
BFS:A->B->C->D
如果使用深度优先的算法,C重载了D的一个方法,会导致搜索不到C的重载,只会用到D
那么针对这种菱形继承应该使用BFS。
然而BFS 同样也会具有问题,如图:
D(D) -->B(B)
E(E) -->C(C)
B(B) -->A(A)
C(C) -->A(A)
DFS: A->B->D->C->E
BFS: A->B->C->D->E
针对这种继承如果使用广度优先,C和D有同名方法,正常应该使用D的方法(D,B应为一个整体,B的优先级比C高),但是如果广度优先就会使用到C的方法。
C3 linearization 测试
为了解决以上问题 python3 使用的mro是 c3 linearization 算法,翻译就是 c3线性化算法,也就是本文重点介绍的内容。
可以简单看一下 python3 针对上述俩种继承的解析顺序:
# 菱形继承
class D:
pass
class B(D):
pass
class C(D):
pass
class A(B,C):
pass
print(A.__mro__)
输出:
(<class '__main__.A'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.D'>, <class 'object'>)
class D:
pass
class E:
pass
class B(D):
pass
class C(E):
pass
class A(B,C):
pass
print(A.__mro__)
输出:
(<class '__main__.A'>, <class '__main__.B'>, <class '__main__.D'>, <class '__main__.C'>, <class '__main__.E'>, <class 'object'>)
可以看到,顺序是合理的,但其使用即不是 DFS也不是BFS。其调用算法就是 c3 算法。
C3 linearization 算法原理
首先我们定义几个符号的意义:(因为后面会用到公式表达)
| 符号 | 意义 |
|---|---|
| L | 针对一个类进行解析用L进行表示,例如L(A)表示对类A进行解析 |
| merge | 合并操作的一个函数(后面具体介绍) |
| C | 表示一个类名 |
| B | 表示是C的一个子类,如果多个子类用B1,B2....表示 |
| + | 元素列表顺序添加 |
| tail | 去除列表第一个元素,例如 tail([1,2,3,4]) = [2,3,4] |
下面是一个关键定义:
L(C) = C + merge(L(B1) + L(B2) + ...+ )
merge函数是如何合并的:
- 首先选中merge 函数的第一个参数(也是一个列表),按照公式里的描述就是L(B1)。
- 取列表中第一个元素记为h,如果h没有出现其他 列表的
tail中, 那么将其移到 merge函数前,提取出来,并且将这个元素在所有列表中移除,并重复 2。 - 如果出现在其他列表中的
tail中,寻找下一个列表。 - merge 函数所有元素都被移除类创建成功,如果寻找不到下一个列表则创建失败。
看到这里可能有点懵,下面具体举一个例子:
X(X) -->A(A)
Y(Y) -->A(A)
X(X) -->B(B)
Y(Y) -->B(B)
A(A) -->F(F)
B(B) -->F(F)
class X():
pass
class Y():
pass
class A(X, Y):
pass
class B(X, Y):
pass
class F(A, B):
pass
print(F.__mro__)
我们来解析 F的mro顺序,则首先记为 L(F),根据
L(C) = C + merge(L(B1) + L(B2) + ...+ )
公式得到:
L(F) = F + merge(L(A)+L(B))
接下来计算L(A),与L(B):
L(A) = A + merge(L(X),L(Y)) = A + merge([X],[Y]) = [A,X,Y]
L(B) = B + merge(L(X),L(Y)) = B + merge([X],[Y]) = [B,X,Y]
带入 L(F) = L(F) + merge(L(A)+L(B)) 得到:
L(F) = F + merge([A,X,Y],[B,X,Y])
下面是关键merge逻辑理解了,首先根据 merge 的说明 1,选中得到 [A,X,Y], 根据merge的说明2,选中第一个元素 A, 判断A 是否在 tail(B,X,Y) 中,即 A 是否在 [X,Y] 中,不在,将其提出来,得到:
L(F) = F + merge([A,X,Y],[B,X,Y]) = [F,A] + merge([X,Y],[B,X,Y])
接着重复 merge的2,判断 X 是否在 tail(B,X,Y)=[X,Y] 中,结果是存在,那么寻找[X,Y]的下一个列表,即[B,X,Y],判断B 是否存在 tail([X,Y])=[Y] 中,不存在,提出B,得到:
L(F) = F + merge([A,X,Y],[B,X,Y]) = [F,A] + merge([X,Y],[B,X,Y]) = [F,A,B] + merge([X,Y],[X,Y])
剩下逻辑一样,依次提出 X和Y:
L(F) = F + merge([A,X,Y],[B,X,Y]) = [F,A] + merge([X,Y],[B,X,Y]) = [F,A,B] + merge([X,Y],[X,Y]) = [F,A,B,X,Y]
可以将我上述python代码运行一下结果和我们手算的是一样的:
(<class '__main__.F'>, <class '__main__.A'>, <class '__main__.B'>, <class '__main__.X'>, <class '__main__.Y'>, <class 'object'>)
复杂的解析(练手逻辑)
O(O) --> C(C)
O(O) --> A(A)
O(O) --> B(B)
O(O) --> D(D)
O(O) --> E(E)
C(C) --> K1(K1)
A(A) --> K1(K1)
B(B) --> K1(K1)
A(A) --> K3(K3)
D(D) --> K3(K3)
B(B) --> K2(K2)
D(D) --> K2(K2)
E(E) --> K2(K2)
K1(K1) --> Z(Z)
K3(K3) --> Z(Z)
K2(K2) --> Z(Z)
L(K1) = K1 + merge(L(C), L(A), L(B))
= K1 + merge([C, O], [A, O], [B, O])
= [K1, C] + merge([O], [A, O], [B, O])
= [K1, C, A] + merge([O], [O], [B, O])
= [K1, C, A, B] + merge([O], [O], [O])
= [K1, C, A, B, O]
L(K2) = [K2, B,D,E, O]
L(K3) = [K3,A, D, O]
L(Z) = Z + merge(L(K1), L(K3), L(K2))
= Z + merge([K1, C, A, B, O],[K3, A, D, O],[K2, B, D, E, O])
= [Z, K1] + merge([C, A, B, O], [K3, A, D, O], [K2, B, D, E, O])
= [Z, K1, C] + merge([A, B, O], [K3, A, D, O], [K2, B, D, E, O])
= [Z K1, C] + merge([A, B, O], [K3, A, D, O], [K2, B, D,E, O])
= [Z, K1, C, K3] + merge([A, B, O], [A, D, O], [K2, B, D, E, O])
= [Z, K1, C, K3, A] + merge([B, O], [D, O], [K2, B, D, E, O])
= [Z,K1, C, K3, A, K2] + merge([B, O], [D, O], [B, D, E, O])
= [Z,K1, C, K3, A, K2, B] + merge([O], [D, O], [D, E, O])
= [Z, K1,C, K3, A, K2, B, D] + merge([O], [O], [E,O])
= [Z, K1,C, K3, A, K2, B, D, E, O]
class O:
pass
class C(O):
pass
class A(O):
pass
class B(O):
pass
class D(O):
pass
class E(O):
pass
class K1(C,A,B):
pass
class K3(A,D):
pass
class K2(B,D,E):
pass
class Z(K1,K3,K2):
pass
print(Z.__mro__)
其实我看到很多文章有这种写法:
L(K1) = K1 + merge(L(C), L(A), L(B),(C,A,B))
这个(C,A,B)写不写都可以,最后都是要删除的,很多国外网站文章习惯这么写,应该是便于理解。
手写一个C3 linearization 算法
理解了merge的原理,我想我可以简单实现一下这个算法,可能你已经想象到了针对 L 的函数需要用到递归实现,merge参数传递一个二维数组就可以。
class O:
pass
class C(O):
pass
class A(O):
pass
class B(O):
pass
class D(O):
pass
class E(O):
pass
class K1(C,A,B):
pass
class K3(A,D):
pass
class K2(B,D,E):
pass
class Z(K1,K3,K2):
pass
import copy
# merge_list 为一个二维的数组
def merge(merge_list):
index = 0
res = []
while index < len(merge_list):
if "".join(["".join(i) for i in merge_list]) == "":
break
if merge_list[index] == []:
index += 1
first = merge_list[index][0]
t = copy.deepcopy(merge_list)
t.pop(index)
temp_all = "".join(["".join(i[1:]) for i in t])
if first not in temp_all:
for temp_list in merge_list:
if first in temp_list:
temp_list.remove(first)
res.append(first)
else:
index += 1
return res
def L(arg_class):
if arg_class.__bases__[0].__name__ == 'object':
return [arg_class.__name__]
res = [arg_class.__name__]
res += merge([L(clss) for clss in arg_class.__bases__])
return res
print(Z.__mro__)
print(L(Z))
我也没好好优化这个算法,反正能跑通,另外无法测试 错误继承,因为错误继承在类的实现的时候就会报错,为了方便测试我自己算法是否正确(看看__mro__属性就可以了),类的继承使用和python内置继承,没有自己写继承逻辑。
c3 linearization详解的更多相关文章
- c3算法详解
c3 算法求某一类在多继承中的继承顺序:类的mro == [类] + [父类的继承顺序] + [父类2的继承顺序]如果从左到右的第一个类在后面的顺序中出现,那么就提取出来到mro顺序中[ABCD] + ...
- org.apache.log4j.Logger详解
org.apache.log4j.Logger 详解 1. 概述 1.1. 背景 在应用程序中添加日志记录总的来说基于三个目的 :监视代码中变量的变化情况,周期性的记录到文件中供其他应用进行统计分析工 ...
- C#中string.format用法详解
C#中string.format用法详解 本文实例总结了C#中string.format用法.分享给大家供大家参考.具体分析如下: String.Format 方法的几种定义: String.Form ...
- Log4j配置详解(转)
一.Log4j简介 Log4j有三个主要的组件:Loggers(记录器),Appenders (输出源)和Layouts(布局).这里可简单理解为日志类别,日志要输出的地方和日志以何种形式输出.综合使 ...
- [Java入门笔记] 面向对象编程基础(二):方法详解
什么是方法? 简介 在上一篇的blog中,我们知道了方法是类中的一个组成部分,是类或对象的行为特征的抽象. 无论是从语法和功能上来看,方法都有点类似与函数.但是,方法与传统的函数还是有着不同之处: 在 ...
- Log4J日志配置详解
一.Log4j简介 Log4j有三个主要的组件:Loggers(记录器),Appenders (输出源)和Layouts(布局).这里可简单理解为日志类别,日志要输出的地方和日志以何种形式输出.综合使 ...
- Linux C 字符串输入函数 gets()、fgets()、scanf() 详解
一.gets() 函数详解 gets()函数用来从 标准输入设备(键盘)读取字符串直到 回车结束,但回车符('\n')不属于这个字符串. 调用格式为: gets(str); 其中str为字符串变量(字 ...
- JMeter学习-023-JMeter 命令行(非GUI)模式详解(一)-执行、输出结果及日志、简单分布执行脚本
前文 讲述了JMeter分布式运行脚本,以更好的达到预设的性能测试(并发)场景.同时,在前文的第一章节中也提到了 JMeter 命令行(非GUI)模式,那么此文就继续前文,针对 JMeter 的命令行 ...
- Netsuite Formula > Oracle函数列表速查(PL/SQL单行函数和组函数详解).txt
PL/SQL单行函数和组函数详解 函数是一种有零个或多个参数并且有一个返回值的程序.在SQL中Oracle内建了一系列函数,这些函数都可被称为SQL或PL/SQL语句,函数主要分为两大类: 单行函数 ...
- Log4J详解
Log4J 简介 Log4j有三个主要的组件:Loggers(记录器),Appenders (输出源)和Layouts(布局).这里可简单理解为日志类别,日志要输出的地方和日志以何种形式输出.综合使 ...
随机推荐
- Form表单数据
官方文档地址:https://fastapi.tiangolo.com/zh/tutorial/request-forms/ 接收的不是 JSON,而是表单字段时,要使用 Form 要使用表单,需预先 ...
- PostgreSQL 创建数据库
PostgreSQL 创建数据库可以用以下三种方式: 1.使用 CREATE DATABASE SQL 语句来创建. 2.使用 createdb 命令来创建. 3.使用 pgAdmin 工具. CRE ...
- Prometheus自身的监控告警规则
1.先在 Prometheus 主程序目录下创建rules目录,然后在该目录下创建 prometheus-test.yml文件,内容如下: 内容很多,可以根据实际情况进行调整. 规则参考网址:http ...
- Ubuntu20.04本地安装Redash中文版
一.安装基础环境: # 1.更换APT国内源 sudo sed -i s@/cn.archive.ubuntu.com/@/mirrors.aliyun.com/@g /etc/apt/sources ...
- netstat -lnp |grep XXX后不显示进程
netstat -lnp |grep XXX后不显示进程,不一定是没有进程,可能是这个命令不好使,换成 ps -ef | grep XXX
- PAT (Basic Level) Practice 1024 科学计数法 分数 20
科学计数法是科学家用来表示很大或很小的数字的一种方便的方法,其满足正则表达式 [+-][1-9].[0-9]+E[+-][0-9]+,即数字的整数部分只有 1 位,小数部分至少有 1 位,该数字及其指 ...
- [题解] Topcoder 15279 SRM 761 Div 1 Level 3 SpanningSubgraphs DP,容斥
题目 考虑DP.\(f(msk,i)\) 表示集合 \(msk(一定包含0号点)\) ,选了恰好i条边的连通方案数.转移用容斥,用这个点集内部所有连边方案减去不连通的.令\(|e_{msk}|\)表示 ...
- vivo互联网机器学习平台的建设与实践
vivo 互联网产品团队 - Wang xiao 随着广告和内容等推荐场景的扩展,算法模型也在不断演进迭代中.业务的不断增长,模型的训练.产出迫切需要进行平台化管理.vivo互联网机器学习平台主要业务 ...
- 如何用AR Engine环境Mesh能力实现虚实遮挡
在AR应用中,用户最不希望看到不真实的穿模现象发生,如虚拟形象部分身体陷入墙壁之中,或者未碰到墙壁却已无法移动,这种不真实的交互十分影响用户体验.那如何才能让避免虚拟物体的穿模问题呢?使用AR Eng ...
- 【Java】Java中的零拷贝
物理内存 计算机物理内存条的容量,比如我们买电脑会关注内存大小有多少G,这个容量就是计算机的物理内存. 虚拟内存 操作系统为每个进程分配了独立的虚拟地址空间,也就是虚拟内存,虚拟地址空间又分为用户空间 ...