Python 进阶技巧: 类与继承

 Python  类  MRO  继承 󰈭 3409字

本文主要介绍有关 Python 中类(class)与继承(inheritance)的一些高级使用知识与技巧, 包括:

  • 与类相关的若干内置函数

  • 多继承下的 MRO 序列与 C3 序列化方法

  • super 类的使用

  • 魔术方法与类的实例化过程

为什么会入坑 Python 了?

在看公司一份超大的历史遗留项目时, 发现不仅仅有装饰器、描述器、大量的父子继承关系(实不相瞒, 太多的继承已经使得这份代码非常难以维护)、甚至还有for break这种此前从未见过的语法..
因为入门 Python 还是靠我的本科毕设写了一些简单的 Python 机器学习/神经网络代码, 还远称不上精通.. 因此也是顺带着去看了一些 Python 的高级编程技巧, 这种纯动态的、对OOP支持良好的、万物皆对象的语言确实是给我带来了很大的新奇感, 在我的 C & shell & Rust 的主要编程语系中掀起了不小的波兰. 所以就发现 Python 语言的特性还蛮好玩的, 而且 CPython 是基于 C 写的, 让人又多了几分亲切感.
综合上述的考量, 可能会稍微对 Python 的各种特性和技巧进行一个深入的研究. :)

两个相关的内置函数

Of course, a language feature would not be worthy of the name “class” without supporting inheritance.

继承是OOP的一大特性, Python 拥有两个相关的内置函数用于判断继承关系:

issubclassisinstance:

  • 使用 isinstance() 来检查一个实例的类型: isinstance(obj, int) 仅会在 obj.__class__ 为 int 或某个派生自 int 的类时为 True.

  • 使用 issubclass() 来检查类的继承关系: issubclass(bool, int) 为 True, 因为 bool 是 int 的子类. 但是, issubclass(float, int) 为 False, 因为 float 不是 int 的子类.

参考: 9. Classes — Python 3.12.1 documentation

MRO 与 C3 算法

C3算法(C3 superclass linearization)算法主要用于确定多重继承时, 子类应该继承哪一个父类的方法, 即方法解析顺序(Method Resolution Order, MRO).

为什么被称为C3?

C3 superclass linearization is called C3 because it “is consistent with three properties”:

  • a consistent extended precedence graph, (保持继承拓扑图的一致性)
  • preservation of local precedence order, and (保证局部优先原则; 比如A继承C, C继承B, 那么A读取父类方法, 应该优先使用C的方法而不是B的方法)
  • fitting a monotonicity criterion. (保证单调性原则, 即子类不改变父类的方法搜索顺序)

在Python2.3之前是基于深度优先算法, 为了解决原来基于dfs算法不满足本地优先级, 和单调性以及继承不清晰的问题, 从Python2.3起应用了新的C3算法.

C3 序列化算法原理

若类型B继承了多个基类:

1class B(A1,A2,...,An)

则 B 的 mro 序列可以通过以下递归式获得:

1mro(B) = [B] + merge(mro(A1), mro(A2),...,mro(An), [A1,A2,...,An])

其中 merge是一个函数操作, 接收多个序列作为参数, 并返回一个参数; 其工作流程为:

  • 遍历序列, 检查当前遍历到的序列的第一个元素:

    • 若其在其他序列中也是第一个元素, 或不在其他序列出现, 则将该元素append到返回序列中, 并从所有merge操作序列中删除;

    • 不然, 则基序遍历, 重复上述操作

  • 不断执行上述的遍历操作, 直到 merge 操作序列为空

C3 算法实例

1class A(object):pass
2class B(object):pass
3class C(object):pass
4class E(A,B):pass
5class F(B,C):pass
6class G(E,F):pass

在手算的时候, 不能从下往上算, 因为纸笔难做到记忆化和递归, 从基类不断往下算比较好

A, B, C的 mro 序列易知为 [A,O][B,O][C,O]

E 的 mro 序列为:

1mro(E) = [E] + merge(mro(A), mro(B), [A,B])
2       = [E] + merge([A,O], [B,O], [A,B])
3       = [E,A] + merge([O], [B,O], [B])
4       = [E,A,B] + merge([O], [O])
5       = [E,A,B,O]

类似的也容易算出F和G的mro序列为: [F,B,C,O][G,E,A,F,B,C,O]

C3 算法 python 实现

talk is cheap, 这份代码的实现可以加深对C3算法的理解.

 1def c3MRO(cls):
 2    if cls is object:
 3        # 讨论假设顶层基类为object,递归终止
 4        return [object]
 5
 6    # 构造C3-MRO算法的总式,递归开始
 7    mergeList = [c3MRO(baseCls) for baseCls in cls.__bases__]
 8    mergeList.append(list(cls.__bases__))
 9    mro = [cls] + merge(mergeList)
10    return mro
11
12
13def merge(inLists):
14    if not inLists:
15        # 若合并的内容为空,返回空list
16        # 配合下文的排除空list操作,递归终止
17        return []
18
19    # 遍历要合并的mro
20    for mroList in inLists:
21        # 取head
22        head = mroList[0]
23        # 遍历要合并的mro(与外一层相同),检查尾中是否有head
24        ### 此处也遍历了被取head的mro,严格地来说不符合标准算法实现
25        ### 但按照多继承中地基础规则(一个类只能被继承一次),
26        ### head不可能在自己地尾中,无影响,若标准实现,反而增加开销
27        for cmpList in inLists[inLists.index(mroList) + 1:]:
28            if head in cmpList[1:]:
29                break
30        else:
31            # 筛选出好head
32            nextList = []
33            for mergeItem in inLists:
34                if head in mergeItem:
35                    mergeItem.remove(head)
36                if mergeItem:
37                    # 排除空list
38                    nextList.append(mergeItem)
39            # 递归开始
40            return [head] + merge(nextList)
41    else:
42        # 无好head,引发类型错误
43        raise TypeError

此外, 使用 python 自带的 .mro()方法也可以获取 mro 序列.

参考:

super 的使用

明白了 mro 的基本原理后就容易理解如何使用 super 了.

这里需要注意, super 是一个类, 而不是一个方法!

super(class, obj) 接收两个参数, 返回的是 obj 的 mro 序列中的 class 类的父类, 其中 obj 可以是一个类也可以是一个实例.

当不传入参数给 super时, 其将(1)寻找自己在哪个类中被定义, 并把类名作为第一个参数传入; (2)寻找自己在哪个函数中被定义, 并把该函数的第一个参数拿过来, 作为自己的第二个参数传入.

多继承下的 super

多继承的情况并没有什么不同, 只要搞清楚 mro 序列的情况就能很清楚地知道super到底会调用谁的方法了.

比如:

 1class A:
 2    def __init__(self):
 3        print('A')
 4class B:
 5    def __init__(self):
 6        print('B')
 7class C(A,B):
 8    def __init__(self):
 9        super(C,self).__init__()
10        print('C')
11class D(B,A):
12    def __init__(self):
13        super(B,self).__init__()
14        print('D')
15print(C.__mro__)
16print(D.__mro__)
17print('initi C:')
18c = C()
19print('initi D:')
20d = D()
21
22# (<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>)
23# (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>)
24# initi C:
25# A
26# C
27# initi D:
28# A
29# D

可以看到, super 将访问到对应的类在 实例 mro 序列中的后一个类.

super 调用父类方法

借助 super 可以直接调用父类方法, 不过此时此时父类中 self 并不是父类的实例而是子类的实例, 下面的代码能够较好的说明这个问题:

 1class A:
 2    def __init__(self):
 3        self.n = 2
 4
 5    def add(self, m):
 6        print('self is {0} @A.add'.format(self))
 7        self.n += m
 8
 9
10class B(A):
11    def __init__(self):
12        self.n = 3
13
14    def add(self, m):
15        print('self is {0} @B.add'.format(self))
16        super().add(m)
17        self.n += 3
18
19b = B()
20b.add(2)
21print(b.n)
22
23# self is <__main__.B object at 0x106c49b38> @B.add
24# self is <__main__.B object at 0x106c49b38> @A.add
25# 8

参考:

类的创建过程

1class A:
2    name = "AAA"
3    def f(self):
4        print("f")
5
6print(A.__dict__)
7
8# {'__module__': '__main__', 'name': 'AAA', 'f': <function A.f at 0x105b340d0>, '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None}

以该类为例, 当解释器遇到class A的定义代码后, 将会运行 class 内部所有的代码, 随后将产生的所有局部变量的内容存在__dict__字典下; 再调用内置函数 __build_class__ , 它返回一个 type类型的对象, 然后把这个 type 的对象保存到类名这个变量里, 这就是一个典型的静态创建类的过程.

至此也可以发现, type 其实是一个元类, 即创建类的类, 所有的类都是 type 的实例.

那么就自然可以给出一种动态创建类的方法:

1def f(self):
2    print("f")
3dic = {
4    "name": "AAA",
5    "f": f
6}
7
8A = type('A', (), dic)

type(x1, x2, x3)返回一个 type 的实例: 其中第一个参数是类的名字; 第二个参数是这个类的父类; 第三个参数一个字典, 规定类的属性和方法.

在一些场景下类的某些属性依赖于动态参数, 就可以使用这种基于type的动态创建类的方式.

type 为什么又可以查看类型也可以创建类?

type 既可以查看 object 的类型(1个参数), 又可以创建类(3个参数), 这是很令人困惑, 也容易出错的; 这大概是 Python 的历史包袱吧.

参考: Python 深耕(三) - 知乎

魔术方法与类的实例化

在类的实例化过程中, 主要关注__new__, __init__ 以及__call__这三个魔术方法.

在一个类的实例化过程中, 如Foo(*args, **kwargs):

  • Foo 类的调用等价于调用 Foo.__call__(*args, **kwargs); 不过 Foo 类本身未必实现了 __call__方法, 因而其将使用其父类type的方法, 而type.__call__方法是使用C实现的, 其大致等价于:

    1def __call__(obj_type, *args, **kwargs):
    2    obj = obj_type.__new__(*args, **kwargs)
    3    if obj is not None and issubclass(obj, obj_type):
    4	obj.__init__(*args, **kwargs)
    5    return obj
  • type.__call__ 将进一步调用 type.__new__(Foo, *args, **kwargs), 返回 Foo 类型的实例.

  • 调用 obj 的实例方法 __init__, 对实例进行初始化

  • 返回实例 obj

当需要重写 __new__ 方法时, 因为当前的__new__已经被覆盖了, 需要借助 super.__new__方法来构造实例:

 1class Example1(object):
 2    def __new__(cls):
 3        return super(Example1, cls).__new__(cls)
 4
 5
 6class Example2(object):
 7    def __new__(cls):
 8        pass
 9
10
11print('For Example1: %s' % Example1())
12print('For Example2: %s' % Example2())
13
14# For Example1: <__main__.Example1 object at 0x021B36B0>
15# For Example2: None

元类的重写

type本身也是一个类, 那么也可以继承他:

1class M(type):
2    def __new__(cls, name, bases, dict):
3        return type.__new__(cls, name, bases, dict)
4    
5    def __init__(self, name, bases, dict):
6        return type.__init__(self, name, bases, dict)
7    
8    def __call__(cls, *args, **kwds):
9        return type.__call__(cls, *args, **kwds)

所有继承了 type 类的都是元类, 使用自定义的元类可以控制其他类的生成方式; 比如:

1# 静态方法
2class A(metaclass=M):
3	...
4
5# 动态方法
6A = M('A', (), dic)

在类的定义中指定metaclass就可以要求解释器按照指定的元类的规则去生成类; 或者可以直接通过动态实例化的方式直接生成对应的元类的实例.

通过自定义元类的魔术方法, 可以在不同的阶段做一些额外的事情:

  • 自定义 __new__: 在类建立的过程中动些手脚;

  • 自定义 __init__: 在对类做初始化的时候动些手脚, 比如给这个类绑定一些属性, 即使在类的定义中没有这些属性;

  • 自定义 __call__: 对这个类做实例化的时候动些手脚, 典型应用是实现单例 (single instance). 在元类中实现单例吗? 应该是在类的__call__中实现单例?

参考:

嗨! 这里是 rqdmap 的个人博客, 我正关注 GNU/Linux 桌面系统, Linux 内核, 后端开发, Python, Rust 以及一切有趣的计算机技术! 希望我的内容能对你有所帮助~
如果你遇到了任何问题, 包括但不限于: 博客内容说明不清楚或错误; 样式版面混乱等问题, 请通过邮箱 rqdmap@gmail.com 联系我!
修改记录:
  • 2024-01-04 23:55:50Python 进阶技巧: 类与继承
Python 进阶技巧: 类与继承