Python面向对象——super()
Python OOP中关于super()的描述
在Python OOP实际应用中,有一个函数super(),通过这个函数可以允许继承父类的子类访问该父类中的方法。super()会在该子类当中单独返回一个父类的临时对象,然后允许你在该子类中调用该父类所拥有的方法。通常使用super()这个函数调用父类当中所构建的方法,这可以使你无需在子类中重写这些方法,并允许你使用最少的代码更改来替换父类。在对Python super()有了一个大致的认识后,下面说一说Python中关于各种继承(单继承、多继承)对super()的简单应用等。
Python单继承之super()
首先来定义两个类:Rectangle、Square,通过这两个类来说明继承关系。定义类如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31class Rectangle():
'''矩形类'''
def __init__(self, width, height):
self.width = width
self.length = length
def area(self):
'''计算面积'''
return self.length * self.width
def perimeter(self):
'''计算周长'''
return (self.width + self.length) << 1
class Square():
'''正方形类'''
def __init__(self, length):
self.length = length
def area(self):
'''计算面积'''
return self.length * self.length
def perimeter(self):
'''计算周长'''
return self.length << 2
square = Square(4) # 实例化正方形类
print(square.area())
rectangle = Rectangle(2, 4) # 实例化矩形类
print(rectangle.area())
通过此示例发现,这两个类是两个相互有关联的形状类:正方形是一种特殊的矩形, 但以上的代码似乎并没有展示出这种关系,因此这两个类具有重复的代码。找出了相关性,我们就可以通过这种相关性类实现继承的关系,通过继承减少代码的量的编写:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23class Rectangle():
'''矩形类'''
def __init__(self, width, length):
self.width = width
self.length = length
def area(self):
'''计算面积'''
return self.length * self.width
def perimeter(self):
'''计算周长'''
return (self.width + self.length) << 1
class Square(Rectangle):
'''正方形类'''
def __init__(self, length):
super().__init__(length, length)
square = Square(4) # 实例化正方形类
print(square.area())
rectangle = Rectangle(2, 4) # 实例化矩形类
print(rectangle.area())
在这里,我们使用了super()来调用Rectangle类的init (),允许我们在Square类中使用它而不重复代码。 如下所示,核心功能在进行更改后仍然存在。
因为Square和Rectangle . init ()方法非常相似,所以你可以使用super()从Square的方法中调用超类的. init ()方法(Rectangle . init ())。 这里设置了.length和.width属性,即使您只需要为Square构造函数提供单个长度参数。
当你运行它时,即使你的Square类没有显式地实现它,对.area()的调用将使用超类中的.area()方法并打印16. Square类从Rectangle继承.area() 方法。
super()在单继承中能为你做什么呢?
与其他面向对象语言一样,它允许您在子类中调用父类的方法。这种情况的主要用例是扩展继承方法的功能。
在下面的示例中,您将创建一个继承自Square的类Cube,并扩展.area()的功能(通过Square继承自Rectangle类)以计算Cube实例的表面积和体积:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34class Rectangle():
'''矩形类'''
def __init__(self, width, length):
self.width = width
self.length = length
def area(self):
'''计算面积'''
return self.length * self.width
def perimeter(self):
'''计算周长'''
return (self.width + self.length) << 1
class Square(Rectangle):
'''正方形类'''
def __init__(self, length):
super().__init__(length, length)
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
这时,让我们看一下边长为3的立方体的表面积和体积:1
2
3cube = Cube(3)
print(cube.surface_area()) # 54
print(cube.volume()) # 27
从上面可以看出,我们为Cube立方体类实现了两个方法:.surface_area()和.volume()。这两个计算都依赖于计算单个面的面积,因此您不必重新实现面积计算,而是使用super()来扩展面积计算。
另外请注意,Cube类定义没有. init ()。因为Cube继承自Square,而. init ()并没有为Cube做任何不同的事情,所以你可以跳过定义它,并且将自动调用超类(Square)的. init ()。
super()将委托对象返回给父类,因此您可以直接调用它所需的方法:super().area()。这不仅使我们不必重写面积计算方法,而且还允许我们在一个位置更改内部.area()逻辑。当你有一些继承自一个父类的子类时,这尤其有用。
介绍了上面这些示例,下面我们探索一下super()它的机制。
super()机制
我们发现,上面一系列的示例是在没有任何参数的情况下调用super()的,然而super()也可以使用两个参数:第一个是子类,第二个参数是作为该子类实例的对象。
首先,让我们看两个示例,通过使用已有的类来展示操作第一个变量可以做什么:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class Rectangle():
'''矩形类'''
def __init__(self, width, length):
self.width = width
self.length = length
def area(self):
'''计算面积'''
return self.length * self.width
def perimeter(self):
'''计算周长'''
return (self.width + self.length) << 1
class Square(Rectangle):
'''正方形类'''
def __init__(self, length):
super(Square, self).__init__(length, length)
在Python 3当中,super(Square,self)调用等同于无参数的super()调用。 第一个参数指的是子类Square,而第二个参数指的是Square对象,在这种情况下,它是self。 我们也可以使用其他类调用super():1
2
3
4
5
6
7
8
9
10
11class Cube(Square):
'''立方体类'''
def surface_area(self):
'''计算表面积'''
face_area = super(Square, self).area()
return face_area * 6
def volume(self):
face_area = super(Square, self).area()
return face_area * self.length
在此示例中,您将Square设置为super()的子类参数,而不是Cube。 这导致super()开始在实例层次结构中的Square上方的一个级别搜索匹配方法(在本例中为.area()方法),在本例中为Rectangle类。在此特定示例中,行为不会更改。 但想象一下Square还实现了一个你想要确保Cube不使用的.area()方法。 以这种方式调用super()可以让你这样做。
第二个参数怎么样?请记住,这是一个对象,它是用作第一个参数的类的实例。例如,isinstance(Cube,Square)必须返回True。
通过包含实例化对象,super()返回一个绑定方法:绑定到对象的方法,用来为方法提供对象的上下文,例如任何实例属性。如果未包含此参数,则返回的方法只是一个函数,与对象的上下文无关。(注意:super()不返回方法。它返回一个代理对象。它将调用正确的类方法,而不需要另外创建一个对象。)
Python多继承之super()
了解了单继承的super()和它的一些示例,下面,我们将开始了解多继承的一些概述和一些示例,这些示例将演示多继承如何工作以及super()如何启用该功能。
- 多继承概述
Python支持多继承,其中子类可以从多个不必继承的超类(也称为兄弟类)继承。
为了更好地说明多继承的实际应用,下面将展示如何通过三角形和正方形构建一个右金字塔(带有方形底座的金字塔):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46class Rectangle():
'''矩形类'''
def __init__(self, width, length):
self.width = width
self.length = length
def area(self):
'''计算面积'''
return self.length * self.width
def perimeter(self):
'''计算周长'''
return (self.width + self.length) << 1
class Square(Rectangle):
'''正方形类'''
def __init__(self, length):
super().__init__(length, length)
class Triangle():
'''三角形类'''
def __init__(self, base, height):
self.base = base
self.height = height
def area(self):
return self.base * self.height * 0.5
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 perimeter * self.slant_height * 0.5 + base_area
此示例原来的基础上又声明了一个Triangle类和一个继承Square和Triangle的RightPyramid类。
您将看到另一个使用super()的.area()方法,就像在单继承中一样,目的是用到在Rectangle类中一直定义的.perimeter()和.area()方法。
但问题是两个超类(Triangle和Square)都定义了一个.area()。花一点时间思考在RightPyramid上调用.area()时会发生什么,然后尝试调用它,如下所示:1
2
3
4
5
6
7
8
9
10pyramid = RightPyramid(2, 4)
print(pyramid.area())
# Traceback (most recent call last):
# File "C:\Users\lenovo\Desktop\dianping.py", line 103, in <module>
# print(pyramid.area())
# File "C:\Users\lenovo\Desktop\dianping.py", line 99, in area
# base_area = super().area()
# File "C:\Users\lenovo\Desktop\dianping.py", line 88, in area
# return self.base * self.height * 0.5
# AttributeError: 'RightPyramid' object has no attribute 'height'
您是否猜测Python会尝试调用Triangle.area()? 这是因为所谓的方法解析顺序在起作用。
注意:我们怎么注意到Triangle.area()被调用了,而不是像我们希望的那样,Square.area()? 如果查看回溯的最后一行(在AttributeError之前),您将看到对特定代码行的引用:1
return self.base * self.height * 0.5
您可以将几何类中的这个方法认为是三角形面积公式。 或者,你可能已经找到Triangle和Rectangle类定义,并在Triangle.area()中看到了相同的代码。
- 方法解析顺序mro
方法解析顺序告诉Python如何搜索继承来的方法,当你使用super()时,这回排上很大的用场,因为mro会告诉Python将使用super()以及以什么样的顺序来进行调用的方法。
每个类都会有一个.mro的魔术属性,它允许我们检查顺序:1
2
3print(RightPyramid.__mro__)
# 打印结果:
(<class '__main__.RightPyramid'>, <class '__main__.Triangle'>, <class '__main__.Square'>, <class '__main__.Rectangle'>, <class 'object'>)
这告诉我们首先在Rightpyramid中搜索方法,然后在Triangle中搜索,然后在Square中搜索,然后在Rectangle中搜索,然后,如果没有找到,则在对象中搜索。
这里的问题是解释器在Square和Rectangle之前在Triangle中搜索.area(),并且在Triangle中找到.area()时,Python会调用它而不是你想要的那个。 因为Triangle.area()期望有.height和.base属性,所以Python会抛出AttributeError。
幸运的是,您可以控制MRO的构建方式。 只需更改RightPyramid类的签名,即可按所需顺序进行搜索,方法将正确解析:1
2
3
4
5
6
7
8
9
10
11
12class 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 perimeter * self.slant_height * 0.5 + base_area
请注意,RightPyramid使用Square类中的. init ()进行部分初始化。 这允许.area()在对象上使用.length,如设计的那样。
现在,您可以构建金字塔类,检查MRO并计算表面积:1
2
3pyramid = RightPyramid(2, 4)
print(pyramid.area()) # 20.0
print(RightPyramid.__mro__) # (<class '__main__.RightPyramid'>, <class '__main__.Square'>, <class '__main__.Rectangle'>, <class '__main__.Triangle'>, <class 'object'>)
您可以看到MRO现在是您所期望的,并且您也可以检查金字塔的表面积,这要归功于.area()和.perimeter()。
不过,这里仍然存在问题。为了简单起见,我在这个例子中故意设置了一些错误:第一个,可以说最重要的是,我有两个具有相同方法名称和签名的独立类。
这会导致方法解析问题,因为将调用MRO列表中遇到的.area()的第一个实例。
当您使用具有多重继承的super()时,必须设计您的类以进行协作。其中一点是确保您的方法是唯一的,以便通过签名确保方法是唯一的 - 无论是使用方法名称还是方法参数,在MRO中正确解析它们。
在这种情况下,为了避免对代码进行彻底检查,可以将Triangle类的.area()方法重命名为.tri_area()。这样,area方法可以继续使用类属性而不是使用外部参数:1
2
3
4
5
6
7
8
9
10class Triangle():
'''三角形类'''
def __init__(self, base, height):
self.base = base
self.height = height
super().__init__()
def tri_area(self):
return self.base * self.height * 0.5
让我们继续在RightPyramid类中使用它:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class 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 perimeter * self.slant_height * 0.5 + base_area
def area_2(self):
base_area = super().area()
triangle_area = super().tri_area()
return triangle_area * 4 + base_area
这里的下一个问题是代码没有像Square对象那样的委托的Triangle对象,所以调用.area_2()会给我们一个AttributeError,因为.base和.height没有任何值。
你需要做两件事来解决这个问题:
1.使用super()调用的所有方法都需要调用其超类的该方法版本。 这意味着您需要将super(). init ()添加到Triangle和Rectangle的. init ()方法中。
2.重新设计所有. init ()调用以获取关键字字典。 请参阅下面的完整代码。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64class Rectangle():
'''矩形类'''
def __init__(self, width, length, **kwargs):
self.width = width
self.length = length
super().__init__(**kwargs)
def area(self):
'''计算面积'''
return self.length * self.width
def perimeter(self):
'''计算周长'''
return (self.width + self.length) << 1
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 self.base * self.height * 0.5
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 perimeter * self.slant_height * 0.5 + base_area
def area_2(self):
base_area = super().area()
triangle_area = super().tri_area()
return triangle_area * 4 + base_area
此代码中存在许多重要差异:
1.kwargs在某些地方被修改(例如RightPyramid . init ()): 这将允许这些对象的用户仅使用对该特定对象有意义的参数来实例化它们。
2.在kwargs之前设置命名参数:你可以在RightPyramid . init ()中看到这个。 这具有从kwargs字典中去除特定键的简洁效果,因此当它在MRO中最终进行到object时,**kwargs为空。
现在,当您使用这些更新的类时,您有:1
2
3pyramid = RightPyramid(base=2, slant_height=4)
print(pyramid.area()) # 20
print(pyramid.area_2()) # 20.0
起作用了! 您已经使用super()成功追溯复杂的类层次结构,同时使用继承和组合来创建具有最少重新实现的新类。
多重继承替代方案
如您所见,多重继承可能很有用,但也会导致非常复杂的情况和难以阅读的代码。 拥有整齐地从多个其他对象继承所有东西的对象也很少见。
如果您发现自己开始使用多重继承和复杂的类层次结构,那么值得问问自己是否可以通过使用组合而不是继承来实现更清晰,更易于理解的代码。
通过组合,您可以从一个称为mixin的专用简单类中为您的类添加非常特定的功能。
由于本文主要关注继承,因此我不会详细介绍组合以及如何在Python中使用它,但这里有一个使用VolumeMixin为我们的3D对象提供特定功能的简短示例 - 在这种情况下,是一个体积计算:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35class Rectangle():
'''矩形类'''
def __init__(self, width, length):
self.width = width
self.length = length
def area(self):
'''计算面积'''
return self.length * self.width
class Square(Rectangle):
'''正方形类'''
def __init__(self, length):
super().__init__(length, length)
class VolumeMixin():
def volume(self):
return self.area() * self.height
class Cube(VolumeMixin, Square):
'''立方体类'''
def __init__(self, length):
super().__init__(length)
self.height = length
def surface_area(self):
face_area = super().area()
return face_area * 6
def face_area(self):
return super().area()
在这个例子中,代码被重新设计为包含一个名为VolumeMixin的mixin。 然后,Cube使用mixin并使Cube能够计算其体积,如下所示:1
2
3cube = Cube(2)
print(cube.surface_area()) # 24
print(cube.volume()) # 8
这个mixin可以在任何需要计算体积的类中以相同的方式使用,并且公式area*height返回正确的结果。
有关Python中面向对象编程和使用super()的更多信息,请查看以下资源:
- 官方的super()文档
- 由Raymond Hettinger写的Python的super()真的超赞
- Python中面向对象的编程3
参考自:使用Python Super()为类提供继承支持 译者:javylee
英文原文