退出

  • 文章收藏

  • 消息

  • 修改资料

  • 继承和多态,是面向对象编程里的核心内容。Python是支持多重继承的,但是多重继承会引发很多问题,例如同名函数所带来的二义性的问题。

    MRO:方法解析顺序

    在讨论多重继承之前,我们先说说MRO。这一部分,并不是我今天要说的重点,网上对于这方面的讨论有很多,但并不一定便于理解,我只是将其中的精髓和本质再阐述一遍。

    在多重继承里面,一个很重要的问题就是多重继承所带来的二义性。例如一个class继承了多个父类,每一个父类又会有自己继承的父类,如果它们都有一个重名的方法,那么最终方法应该怎么生成呢?这就是MRO所做的事。

    程序语言的设计,特别是像Python这种高级语言,更接近人类的思维模式,具有更高的抽象度。因此从设计上,就要符合人们的预期,让人感觉到“合理”。因此我们可以从“合理性”的角度来理解程序语言的设计。

    为了了解程序为什么这么设计,就必须知道历史上曾经出现过什么样的做法,这样才更容易理解它的优点。

    现在的python MRO使用的是C3算法。算法本身比较复杂,运用了拓补学的一些公式。但是我们只需要知道它所实现的目的就行了:

    1. 每个类中的方法只被查找一次,即唯一性;
    2. 子类必定在父类之前被查找(保证子类重写父类的方法)
    3. 写在前面的类必定在写在后面的类之前被查找(保证写在前面的类优先查找)
    4. 如果满足2,3的情况下有多个路径,那么优先查找它的父类,即深度优先原则

    其中,2是为了保证子类重写父类的方法,我们肯定不希望在子类中重写一个方法,最后在某种情况下调用的是父类的。3是为了保证,写在左边的类优先。这个其实并没太有所谓,因为左右在逻辑上是并列的,只是规定一个顺序而已。4则是为了保证优先继承自己的直接父类(这一点不易阐述,请结合下文中第二个例子进行理解)。

    历史上在C3算法出现之前,曾经出现过完全的“深度优先”和完全的“广度优先”搜索原则。深度优先,即优先从自下而上的方式遍历,直到没有更深的可供便利。而广度优先,即以从左自右的方式,先遍历一个类所有的直接父元素,然后再向上遍历。

    如上图所示,我们以一个简单的例子,来说明这几种方法的优劣。图中的箭头为继承方式,箭头由父类指向子类。

    如果按照深度优先搜索的原则,那么D首先会搜索B,然后再搜索B的父类A。A没有父类,因此接下来会搜索D的下一个父类C。在继续搜索时,由于C的父类A在之前已出现,为保证唯一性,不会再搜索A。因此搜索的顺序为:D->B->A->C。

    这样会带来什么问题?A->C->D这条继承关系被破坏了(不符合我们上文所说的第2条目的)。如果我们在A中定义了一个方法,然后在C中重写(B中没有定义这个方法)。我们会希望这个方法被D继承。但是由于搜索顺序是D->B->A->C,python会先搜索到A方法,所以D中的该方法是从A继承的。这样我们在类C中所做的重写就失效了。这是不符合我们预期的。

    再说说广度优先。按照广度优先的原则,我们先遍历完D的两个父类B和C,然后再遍历A。因此搜索顺序为D->B->C->A, 并没有违背上述的任何一个目的。也就是说,在这种情况下,它是符合我们预期的。

    那么它是不是就是完美的呢?我们看另外一个例子。

    如果我们按照完全广度优先的原则,E会先遍历它的两个父类B和D,然后再遍历A和C。那么搜索顺序为:E->B->D->A->C,不符合上文所说的第4条目的——(在符合1,2,3条目的的前提下的)深度优先原则。

    这样会带来什么问题呢?假设我们在A中定义了一个方法,它会被B继承。同时类B没有对这个方法进行重写。我们在类D中,也定义一个同名的方法。所以,类B和D的实例都会有这个方法。

    按照我们的预期,E在继承B和D时,应该优先继承B的方法。但由于B中并没有直接定义此方法,而是从A继承的,那么按照搜索顺序:E->B->D->A->C,D在A之前,因此最后E所继承的是D的方法,又不符合我们的预期了。

    而按照我们的C3算法,即满足上述4个限制条件的算法,则不存在上述两种方法所带来的问题。

    这两个例子虽然简单,但是如果理解了MRO算法的本质,不管多复杂的继承关系,一眼就可以看出顺序来,道理都是一样的。

    如何理解super函数?

    我们通常认为super函数是“引用一个类所有父类的方法的函数”,但其实这么说是不对的。

    首先,super引用的并不一定是父类,实际上它引用的是MRO序列里的下一个类。它可能是一个父类,也可能不是。

    其次,super仅仅只引用一次,如果被引用的函数里没有super函数,那么不会再继续引用。

    我们看看super函数的定义:

    super(class, self).method(*args, **kwargs)

    class表明的是当前在MRO列表中的位置,通常为super函数所在的类本身。

    self传递自身实例对象,用来计算MRO列表。

    函数执行的操作:

    1. 通过传入的self实例,计算MRO列表
    2. 根据class在MRO表中所处的位置,引用下个类所对应的方法

    为了理解,我们可以看下面这个例子,想想如何在__init__方法里使用super函数:

    按照MRO的C3算法,引用顺序应该为D->B->C->A->F->E。那么,D中定义的super函数会引用B的__init_方法,B引用C,以此类推。

    这里有两个需要注意的地方是,B的super函数此时并没有引用A的__init__方法,而是引用C的方法。

    这里有一个比较常犯的一个错误:以为A是基类,所以不需要在__init__中使用super方法。这是错误的。如果A中没有使用super方法,E和F中的__init__将不会被引用,而是被直接重写,这样你就丢失了E和F中初始化的那些代码。

    super函数所带来的参数不一致的问题

    我们设想下面一个情景:

    class BaseA(object):
        def __init__(self, name, age):
            super(BaseA, self).__init__()
            pass
    
    class BaseB(object):
        def __init__(self, name, gender, weight):
            super(BaseB, self).__init__()
            pass
    
    class Jerk(BaseA, BaseB):
        def __init__(self, name, age, gender, weight):
            super(Jerk, self).__init__(name, age, gender, weight)

    运行时,会报错:

    >>> a = Jerk('ss',4, True,77)
    Traceback (most recent call last):
      File "", line 1, in 
      File "", line 3, in __init__
    TypeError: __init__() takes exactly 3 arguments (5 given)

    这是因为,我们通过Jerk引用了BaseA的__init__函数,但我们却传递了5个参数。那么问题来了,Jerk有2个参数不同的父类,如何给它们传递参数?

    答案是,在super函数中使用不定参数:*args, **kwargs。

    经过改写的代码为:

    class BaseA(object):
        def __init__(self, name, age, *args, **kwargs):
            super(BaseA, self).__init__(name, *args, **kwargs)
            pass
    
    class BaseB(object):
        def __init__(self, name, gender, weight, *args, **kwargs):
            super(BaseB, self).__init__()
            pass
    
    class Jerk(BaseA, BaseB):
        def __init__(self, name, age, gender, weight):
            super(Jerk, self).__init__(name, age=age, gender=gender, weight=weight)

    除此之外,我们还可以直接通过父类的名称来引用父类的方法。例如:

    BaseA.__init__(name, age)

    这样也能解决父类的参数不一致的问题,且没有使用不定参数,貌似更容易理解。但是这样带来的坏处是,对修改的兼容性很差,例如你修改了某一个基类的名称,那么所有类似的引用就都失效了,你必须一个个的去更改,而且很容易发生遗漏和错误。

    虽然这种方法有些缺陷,但仍是可接受的,但绝对不要在同一个继承树里同时使用两种方法,这会使你的代码运行失去控制。

    混用super方法和类名引用所带来的问题

    我们看下面一个例子:

    如果我们混用两种方法会发生什么问题呢?

    class A(object):
        def __init__(self):
            super(A, self).__init__()
            print('A')
    
    class B(A):
        def __init__(self):
            super(B, self).__init__()
            print('B')
    
    class C(A):
        def __init__(self):
            super(C, self).__init__()
            print('C')
    
    class E(object):
        def __init__(self):
            print('E')
    
    class F(E):
        def __init__(self):
            super(F, self).__init__()
            print('F')
    
    class D(B, C, F):
        def __init__(self):
            B.__init__(self)
            C.__init__(self)
            F.__init__(self)
            print('D')

    根据MRO规则,方法搜索的顺序将是:D->B->C->A->F->E,D->C->A->F->E,D->F->E。方法的执行顺序为:

    E->F->A->C->B->     B.__init__(self)所产生

    E->F->A->C->    C.__init__(self)所产生

    E->F->    D.__init__(self)所产生

    D    print('D')所产生

    打印出来的结果为:

    >>> d=D()
    E
    F
    A
    C
    B
    E
    F
    A
    C
    E
    F
    D
    >>>

    也就是说,D所引用的每一个父类的__init__,都在MRO中把其后的类的方法都遍历了一遍。

    所以,永远不要同时使用super和直接引用。

    支付宝 微信 BTC
    支付宝扫一扫,向我打赏
    来源:原创

    声明:本站原创文章采用 BY-NC-SA 创作共用协议,转载时请以链接形式标明本文地址;非原创(转载)文章版权归原作者所有。 ©查看版权声明

  • 白銀の魔法師
  • 所有的信徒都别无二致,所有的信仰都一文不值
  • 发表评论

    你目前的身份是游客,请输入昵称和邮箱! 输入资料 关闭