阅读本文,需要知道python中的类与继承的概念。

1、总述

在python中,通过类的继承机制,可以实现在子类中调用父类的方法,从而避免写重复的代码。但在面临多继承时,如果多个父类中都实现了某个相同的方法,那么必须要通过特殊的机制来告诉子类,要选用哪个方法。

这种机制是通过super实现的,本文将通过简单的例子逐步分析super的用法。

2、super初探

2.1 间接引用的好处

假设我们有如下代码:

# 定义一个长方形类,实现求面积与周长两种方法
class Rectangle:
    def __init__(self, length, width):
        self.length = length
        self.width = width

    def area(self):
        print("You're getting the area...")
        return self.length * self.width
    
    def perimeter(self):
        print("You're getting the perimeter...")
        return 2 * (self.length + self.width)

# 定义一个正方形类,同样实现求面积与周长两种方法
class Square:
    def __init__(self, length):
        self.length = length
    
    def area(self):
        return self.length * self.length
    
    def perimeter(self):
        return 4 * self.length

这两个类没有任何的继承关系。直接实例化并调用相关方法:

>>>r = Rectangle(3, 5)
>>>r.area()
You're getting the area...
15
>>>r.perimeter()
You're getting the perimeter...
16
>>>s = Square(5)
>>>s.perimeter()
20
>>>s.area()
25

我们知道,正方形实际上就是一种长与宽相等的长方形。既然我们在Rectangle类中已经实现了.area().perimeter()方法,那么可以在实例化时传入相等的length与width,求出来的实际上就是正方形的面积与周长了。

但问题是,如果通过上述方法实例化,该实例仍是一个Rectangle对象,而不是Square对象,在一定的情况下,这会带来一些问题。我们希望的是,构造一个Square类的实例,该实例具有.area().perimeter()方法,而不必在Square类中重复编写这些方法的代码。

通过继承,可以完成这一点:

# Rectangle类不变
class Square(Rectangle):
    def __init__(self, length):
        Rectangle.__init__(self, length, length)

先看实例化后的结果:

>>>s = Square(5)
>>>s.perimeter()
You're getting the perimeter...
20
>>>s.area()
You're getting the area...
25

打印的这些「You’re …」表明,调用的.area().perimeter()方法来自于Rectangle类。

Square类的__init__方法只有一行内容,该行代码的意思是:该类在实例化时,会调用其父类Rectangle的实例化方法。父类实例化时需要传入两个参数(length和width),在此处它们的值都是length。

这里需要注意,调用某类的实例化方法实例化某类是不一样的概念——此处的代码里并没有实例化Rectangle

上面的写法虽然可以实现我们最初的目标,但是存在一个问题:如果父类发生了变化,比如说,换了个名字,那么这里的代码就需要改动两处地方:

class Square(OtherClass):
    def __init__(self, length):
        OtherClass.__init__(self, length, length)

这就是所谓的*直接引用(hardwired call)*所带来的弊端。

为了避免这个问题,super就排上了用场。我们可以把上述代码修改如下:

class Square(Rectangle):
    def __init__(self, length):
        super().__init__(length, length)

可以看出,通过使用super,在定义类的代码内部,不再出现父类的名称了,但仍然可以正常引用父类的有关方法。因此,这种引用方法被称为间接引用(indirected call)

2.2 方法扩充

接着上面的例子,现在我们又定义了一个新类:

# 定义一个立方体类,继承自Square,并实现求表面积和体积的方法
class Cube(Square):
    def surface_area(self):
        face_area = super().area()
        return face_area * 6

    def volumn(self):
        face_area = super().area()
        return face_area * self.length

实例化后调用相关方法的结果如下:

>>>c = Cube(3)
>>>c.surface_area()
You're getting the area...
54
>>>c.volume()
You're getting the area...
27

在这个例子中,有两点需要注意:

  1. 我们并没有定义Cube__init__方法,这意味着,在实例化一个Cube对象时所传入的参数和实例化其父类(Square)时完全相同。

  2. 在调用Cube.surface_area().volume()方法时,先会调用父类(Square)的.area()方法;而父类Square.area()方法又是继承自它的父类Rectangle的。

也就是说,通过super(),可以实现对父类方法的扩充,即调用父类方法并构造子类自身的方法,以实现新的功能。

通过2.1与2.2两个例子,我们大概能体会到super所起作用的方式:super().method()会调用父类的方法。如super().area(),表示调用父类的.area()方法求面积;super().__init__()表示调用父类的.__init__()方法。

3、带参数的super

在上面的例子中,调用super时均没有向其中传入任何参数,实际上,super是有两个可选参数的。第一个可选参数是一个类(type),第二个可选参数是一个类(type2)或一个实例(object)。特别地,如果第二个参数是一个object,那么必须有isinstance(object, type)==True;如果第二个参数是一个类,那么必须有issubclass(type2, type)==True

上文中关于SquareRectangle类的代码可以修改如下:

class Rectangle:
    def __init__(self, length, width):
        self.length = length
        self.width = width

    def area(self):
        print("You're getting the area...")
        return self.length * self.width
    
    def perimeter(self):
        print("You're getting the perimeter...")
        return 2 * (self.length + self.width)

class Square(Rectangle):
    def __init__(self, length):
        super(Square, self).__init__(length, length)

这里的super接受了两个参数,第一个为子类,第二个是该类的实例。这其实与直接写super().__init__(length, length)的作用是一样的。

类似地,可以修改Cube类的代码如下:

class Cube(Square):
    def surface_area(self):
        face_area = super(Square, self).area()
        return face_area * 6

    def volumn(self):
        face_area = super(Square, self).area()
        return face_area * self.length

在这里,super的第一个参数是Square而不是Cube,这意味着,super会从Square类的上一层(也就是Rectangle)来寻找匹配的.area()方法。也许你已经想到了,这种用于法可以适用于当Square类中也定义了.area()方法而你却想调用Rectangle.area()方法的情况:

class Square(Rectangle):
    def __init__(self, length):
        super(Square, self).__init__(length, length)

    def area(self):
        print("You're getting Square area...")
        return 0

class Cube(Square):
    def surface_area(self):
        face_area = super().area()
        return face_area * 6

    def volumn(self):
        face_area = super(Square, self).area()
        return face_area * self.length

此时,再实例化Cube并调用相关方法:

>>>c = Cube(9)
>>>c.surface_area()
You're getting Square area...
0
>>>c.volumn()
You're getting the area...
729

可以看出,super().area()会首先匹配到Square中定义的.area()方法并进行调用;而super(Square, self).area()则调用的是Rectangle.area()方法。

另外,传入了第二个参数的super()返回的代理对象所执行的方法是绑定方法(bounded method);否则,就是未绑定方法(unbounded method)。

有关bounded methodunbounded method的内容暂不在本文讨论,读者可以参考这里

在本部分,我们讨论了super()带参数的用法。实际上,不带参数的super()已经可以满足大部分场景中的应用需求了。如果真得到了需要采用带参数的super(),那么首先需要考虑的是,代码的架构是不是可以进行优化。

4、多继承中的super()

在前面的例子中,我们主要讨论了单继承中的super()的用法。读者可能会觉得,super()的作用似乎也没有那么大。实际上,只有在多继承中,super()才能真正发挥它威力。

所谓“多继承”,指的是一个类继承自多个基类,而且这些基类之间是没有互相继承关系的(因此,这些基类也被称为兄弟类,sibling classes)。

4.1 MRO:Method Resolution Order

要想理解多继承,首先需要搞懂MRO的概念。

从名字上来看,它表示方法解析的顺序。更具体地说,python中每一个类都有一个对应的MRO元组,元组里面存储着在调用该类的方法时,解析器的查找顺序。可以通过type.__mro__来查看任意一个类的MRO:

>>>Rectangle.__mro__
(<class '__main__.Rectangle'>, <class 'object'>)

可以看出,对于Rectangle的实例而言,调用它的任何方法,它首先会从Rectangle中找对应的方法,其次是object——object是python中所有类的基类,它实际上没有任何特殊方法以外的方法。

类似地:

>>>Cube.__mro__
(<class '__main__.Cube'>, <class '__main__.Square'>, <class '__main__.Rectangle'>, <class 'object'>)

调用一个Cube实例的方法,它会依次从CubeSquareRectangle中进行寻找。一旦在某个类中匹配到对应的方法,则停止查找。

4.2 super、MRO与多继承

看下面的代码:

class Rectangle:
    def __init__(self, length, width):
        self.length = length
        self.width = width

    def area(self):
        print("You're getting the area...")
        return self.length * self.width

    def perimeter(self):
        print("You're getting the perimeter...")
        return 2 * (self.length + self.width)

class Square(Rectangle):
    def __init__(self, length):
        super(Square, self).__init__(length, length)

class Triangle:
    def __init__(self, base, height):
        # 给定边和对应的高
        self.base = base
        self.height = height

    def area(self):
        return 0.5 * self.base * self.height

class RightPyramid(Triangle, Square):
    def __init__(self, base, slant_height):
        # 给定底面边长和各侧面的斜高
        self.base = base
        self.slant_height = slant_height

    def area(self):
        base_area = super().area()
        perimeter = super().perimeter()
        return 0.5 * perimeter * self.slant_height + base_area

首先,我们定义了一个三角形的类(Triangle),并实现了求面积的方法,这没什么特别需要注意的;接着定义了一个正金字塔形的类(RightPyramid),并实现了求其表面积的方法。

正金字塔,意味着它的底面是一个正方形。

在我们定义的RightPyramid中,我们希望它可以利用Rectangle.area()方法求出底面积,然后利用底面周长*斜高/2来求侧面积,最终相加得到总的表面积。注意,这里的RightPyramid类就是一个多继承类:它继承了TriangleSquare

让我们来尝试运行一下:

>>>pyramid = RightPyramid(2, 4)
>>>pramid.area()
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "<input>", line 41, in area
  File "<input>", line 31, in area
AttributeError: 'RightPyramid' object has no attribute 'height'

这个错误的意思是,在调用RightPyramid的某个父类的.area()时触发了属性不匹配的错误。仔细观察上面的代码,我们虽然试图通过super().area()来调用父类的.area(),但RightPyramid的两个父类TriangleRectangle均实现了.area()方法。从需求出发,我们希望调用的是Rectangle.area()方法,而实际上是这样吗?

可以查看RightPyramid的MRO来帮助判断:

>>>RightPyramid.__mro__
(<class '__main__.RightPyramid'>, <class '__main__.Triangle'>, <class '__main__.Square'>, <class '__main__.Rectangle'>, <class 'object'>)

可以看出,在调用.area()方法时,解释器首先找到了Triangle.area()方法,但该方法的执行需要实例对象有baseheight属性——而RightPyramid实例是没有height属性的,因此而报错。

上述代码的第一个问题是,继承顺序不对。我们希望继承的是Rectangle.area()方法,而非Triangle的,为此,需要调整一下父类的顺序:

class RightPyramid(Square,Triangle):
    pass  # other codes to be added

此外,在求底面积的时候(即调用Rectangle.area()方法时),需要实例具有lengthwidth两个属性。所以,我们将RightPyramid类改写如下:

class RightPyramid(Square,Triangle):
        def __init__(self, base, slant_height):
        # 给定底面边长和各侧面的斜高
        self.base = base
        self.slant_height = slant_height
        super().__init__(base)  # 调用Square的__init__方法,从而使RightPyramid的实例也具有length和width两个属性

    def area(self):
        base_area = super().area()
        perimeter = super().perimeter()
        return 0.5 * perimeter * self.slant_height + base_area

再次执行验证代码,你会发现它可以输出正确的结果。

回顾一下上述过程可以发现,我们在两个不同的类中实现了名称相同的方法(.area()),这导致了子类在继承时会选错。因此,一个良好的编程习惯是,在不同的类中使用不同的方法签名(例如可以把Triangle中的.area()改成.tri_area()),这样无论是从代码可读性,还是维护成本上考虑,都是更优的选择。

例如,遵从上述原则,代码可以修改如下:

class Triangle:
    def __init__(self, base, height):
        # 给定边和对应的高
        self.base = base
        self.height = height

    def tri_area(self):
        return 0.5 * self.base * self.height

class RightPyramid(Square,Triangle):
    def __init__(self, base, slant_height):
        # 给定底面边长和各侧面的斜高
        self.base = base
        self.slant_height = slant_height
        super().__init__(self.base)

    def area(self):
        base_area = super().area()
        perimeter = super().perimeter()
        return 0.5 * perimeter * self.slant_height + base_area

    def area_2(self):
        base_area = super().area()
        triangle_area = super().tri_area()
        return triangle_area * 4 + base_area

这里再调用.area()便不会产生歧义了。

此外,在RightPyramid类中,又实现了.area_2()用于求表面积,这种方法是先求每个侧面的面积,再加上底面积。

但如果你运行pyramid.area_2()时,会触发一个AttributeError,这是因为在调用Triangle.tri_area()方法时,pyramidheight并没有值。也就是说,通过super().method()调用父类的方法时,如果实例缺乏必要的属性值,则会导致调用失败。

为了解决这个问题,我们要做一些稍微复杂的修改:

  1. 对于每一个需要调用父类方法的子类,均需要在其.__init__()方法中调用父类的.__init__()方法,即添加super().__init__()代码;
  2. super().__init__()代码中传入关键字参数的字典。

完整的示例代码如下:

class Rectangle:
    def __init__(self, length, width, **kwargs):
        self.length = length
        self.width = width
        super().__init__(**kwargs)

    def area(self):
        print("You're getting the area...")
        return self.length * self.width

    def perimeter(self):
        print("You're getting the perimeter...")
        return 2 * (self.length + self.width)

class Square(Rectangle):
    def __init__(self, length, **kwargs):
        super().__init__(length=length, width=length, **kwargs)

class Cube(Square):
    def surface_area(self):
        face_area = super().area()
        return face_area * 6

    def volume(self):
        face_area = super().area()
        return face_area * self.length

class Triangle:
    def __init__(self, base, height, **kwargs):
        self.base = base
        self.height = height
        super().__init__(**kwargs)

    def tri_area(self):
        return 0.5 * self.base * self.height

class RightPyramid(Square, Triangle):
    def __init__(self, base, slant_height, **kwargs):
        self.base = base
        self.slant_height = slant_height
        kwargs["height"] = slant_height
        kwargs["length"] = base
        super().__init__(base=base, **kwargs)

    def area(self):
        base_area = super().area()
        perimeter = super().perimeter()
        return 0.5 * perimeter * self.slant_height + base_area

    def area_2(self):
        base_area = super().area()
        triangle_area = super().tri_area()
        return triangle_area * 4 + base_area

上一个问题是,实例没有height属性,从而无法调用.tri_area()方法。在新的写法中,通过将实例化时的slant_height存入kwargs中,并命名为’height’,然后通过super().__init__(base=base, **kwargs)传给父类。当调用到各父类的方法时,每个类再从kwargs中获取该类需要的参数。

在这个例子中,执行super().area()时,Square类获取kwargs中的’length’参数的值;执行super().tri_area()时,Triangle类获取kwargs中的’height’参数的值。

5、总结

本文通过一些示例介绍了python中super的用法。通过super,用户可以在多继承中调用父类中的方法。在使用super时,需要注意以下几点:

  • 通过super调用的方法必须定义在父类中;
  • 调用者和被调用者的参数签名必须一致;
  • 调用每个父类的方法时均需要通过super

有关这三点的详细描述可以在参考内容2中找到。

最后,一定不要试图追求所谓的高级用法而开发一些难以理解的代码,而忽略更加优雅的实现方式。毕竟,pythonic才应该是一个合格的python程序员的追求。

参考内容:

  1. 文章的代码示例及部分内容来自:Supercharge Your Classes With Python super()

  2. Python’s super() considered super!

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐