【python】类继承中super的用法
python中的继承与super()的使用
阅读本文,需要知道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
在这个例子中,有两点需要注意:
-
我们并没有定义
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__
(<class '__main__.Rectangle'>, <class 'object'>)
可以看出,对于Rectangle
的实例而言,调用它的任何方法,它首先会从Rectangle
中找对应的方法,其次是object
——object
是python中所有类的基类,它实际上没有任何特殊方法以外的方法。
类似地:
>>>Cube.__mro__
(<class '__main__.Cube'>, <class '__main__.Square'>, <class '__main__.Rectangle'>, <class 'object'>)
调用一个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 "<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
的两个父类Triangle
和Rectangle
均实现了.area()
方法。从需求出发,我们希望调用的是Rectangle
的.area()
方法,而实际上是这样吗?
可以查看RightPyramid
的MRO来帮助判断:
>>>RightPyramid.__mro__
(<class '__main__.RightPyramid'>, <class '__main__.Triangle'>, <class '__main__.Square'>, <class '__main__.Rectangle'>, <class 'object'>)
可以看出,在调用.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程序员的追求。
参考内容:
更多推荐
所有评论(0)