1、总述阅读本文,需要知道python中的类与继承的概念。
在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
在这个例子中,有两点需要注意:
-
我们并没有定义Cube的__init__方法,这意味着,在实例化一个Cube对象时所传入的参数和实例化其父类(Square)时完全相同。
-
在调用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。
上文中关于Square和Rectangle类的代码可以修改如下:
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 method与unbounded 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__ (, )
可以看出,对于Rectangle的实例而言,调用它的任何方法,它首先会从Rectangle中找对应的方法,其次是object——object是python中所有类的基类,它实际上没有任何特殊方法以外的方法。
类似地:
>>>Cube.__mro__ (, , , )
调用一个Cube实例的方法,它会依次从Cube、Square、Rectangle中进行寻找。一旦在某个类中匹配到对应的方法,则停止查找。
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类就是一个多继承类:它继承了Triangle和Square。
让我们来尝试运行一下:
>>>pyramid = RightPyramid(2, 4) >>>pramid.area() Traceback (most recent call last): File "", line 1, inFile "", line 41, in area File "", line 31, in area AttributeError: 'RightPyramid' object has no attribute 'height'
这个错误的意思是,在调用RightPyramid的某个父类的.area()时触发了属性不匹配的错误。仔细观察上面的代码,我们虽然试图通过super().area()来调用父类的.area(),但RightPyramid的两个父类Triangle和Rectangle均实现了.area()方法。从需求出发,我们希望调用的是Rectangle的.area()方法,而实际上是这样吗?
可以查看RightPyramid的MRO来帮助判断:
>>>RightPyramid.__mro__ (, , , , )
可以看出,在调用.area()方法时,解释器首先找到了Triangle的.area()方法,但该方法的执行需要实例对象有base和height属性——而RightPyramid实例是没有height属性的,因此而报错。
上述代码的第一个问题是,继承顺序不对。我们希望继承的是Rectangle的.area()方法,而非Triangle的,为此,需要调整一下父类的顺序:
class RightPyramid(Square,Triangle): pass # other codes to be added
此外,在求底面积的时候(即调用Rectangle的.area()方法时),需要实例具有length和width两个属性。所以,我们将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()方法时,pyramid的height并没有值。也就是说,通过super().method()调用父类的方法时,如果实例缺乏必要的属性值,则会导致调用失败。
为了解决这个问题,我们要做一些稍微复杂的修改:
- 对于每一个需要调用父类方法的子类,均需要在其.__init__()方法中调用父类的.__init__()方法,即添加super().__init__()代码;
- 在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程序员的追求。
参考内容:
-
文章的代码示例及部分内容来自:Supercharge Your Classes With Python super()。
-
Python’s super() considered super!
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)