前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >开源图书《Python完全自学教程》8.5.2多继承

开源图书《Python完全自学教程》8.5.2多继承

作者头像
老齐
发布2022-07-06 16:13:54
2220
发布2022-07-06 16:13:54
举报
文章被收录于专栏:老齐教室

8.5.2 多继承

顾名思义,“多继承”是指某一个子类的父类不止一个,而是多个。比如:

代码语言:javascript
复制
>>> class P1: p1 = 1
...
>>> class P2: p2 = 2
...
>>> class C(P1, P2): pass
...
>>> dir(C)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'p1', 'p2']

子类 C 继承了两个父类 P1P2 ,它也就具有了两个父类中所定义的类属性 p1, p2

继续改造8.5.1节创建的 rectangle.py 文件,在其中实现多继承。增加如下两个类:

代码语言:javascript
复制
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()       # (6)
        perimeter = super().perimeter()  # (7)
        return 0.5 * perimeter * self.slant_height + base_area

RightPyramid 表示一个底面是正方形、侧面是三角形的正四棱锥。从几何的角度看,这个锥体由正方形和三角形组成,于是让它继承类 TriangleSquare 也顺理成章,此即为多继承的应用。 RightPyramid 类的 __init__() 方法的参数 base 为组成棱锥的正方形边长,slant_height 为组成棱锥的三角形的高。方法 area() 计算正四棱锥的表面积。

为了能清晰地观察到类 RightPyramid 实例化和调用方法的过程,还是进入到交互模式一步一步地执行。注意,现在要求在 rectangle.py 文件所在的目录进入到交互模式中,如图8-5-1所示(在后续内容中,会经常用这种方式对所编写的文件进行调试,并简称为“在文件当前位置进入到交互模式”,请读者知悉)。

图8-5-1 在当前目录进入交互模式

然后执行下述语句(第11章11.1节会详解)。

代码语言:javascript
复制
>>> from rectangle import *
>>> pyramid = RightPyramid(2, 4)

顺利地得到了一个实例 pyramid ,再计算它的面积。

代码语言:javascript
复制
>>> pyramid.area()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/qiwsir/Documents/my_books/codes/rectangle.py", line 53, in area
    base_area = super().area()
  File "/Users/qiwsir/Documents/my_books/codes/rectangle.py", line 45, in area
    return 0.5 * self.base * self.height
AttributeError: 'RightPyramid' object has no attribute 'height'

执行实例的 area() 方法后报错,异常信息显示 RightPyramid 对象没有 height 属性。然而,我们分明在它所继承的一个父类 Triangle 中定义了此属性了,为什么这里还会报错?难道没有“成功”地继承?

问题的根源在于 “方法解析顺序”(Method Resolution Order,简称 MRO)。

在定义 RightPyramid 类时,继承了 Triangle 类和 Square 类,注释(6)使用 super() 调用 RightPyramid 的父类,就要根据 MRO 确定按照什么顺序在父类中搜索有关方法和属性。当然,MRO 是 Python 中已经规定好的,可以用对象的 __mro__ 属性查看:

代码语言:javascript
复制
>>> RightPyramid.__mro__
(<class 'rectangle.RightPyramid'>, <class 'rectangle.Triangle'>, <class 'rectangle.Square'>, <class 'rectangle.Rectangle'>, <class 'object'>)

属性 RightPyramid.__mro__ 也可以用方法 RightPyramid.mro() 替代,两者等效。

上述结果说明,对于类 RightPyramid 而言,首先会搜索它自己内部是否有该方法和属性;然后按照继承顺序,分别搜索两个父类 TriangleSquare (注意顺序);如果仍然未果,则继续搜索 Square 的父类 Rectangle ;还是一无所获,则最终要搜索 object 类。

按照上述顺序,执行 RightPyramid 类中的注释(6)的 super().area() 时,就会在父类 Triangle 中搜索到 Triangle.area(self) 。这个方法中用到两个实例属性 self.baseself.heightself.base 在实例化的时候已经建立( self.base = 2 ),但是 self.height 没有定义。所以抛出了前述 AttributeError 异常。

怎么修改?

注释(6)实则是要计算四棱锥的底面正方形的面积,所以,就可以通过调整父类的顺序,先搜索 Square 类,并且通过调用 Square 类中的初始化方法,为正方形的边长属性( self.base )赋值。在 IDE 中按照下述方式修改代码。

代码语言:javascript
复制
class RightPyramid(Square, Triangle):         # 修改父类顺序
    def __init__(self, base, slant_height):
        self.base = base
        self.slant_height = slant_height
        super().__init__(self.base)           # 调用 Square.__init__()

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

修改之后,回到交互模式。如果仍然没有退出交互模式,需要将修改之后的模块文件 rectangle.py 重新加载——简称“重载”,其方法如下):

代码语言:javascript
复制
>>> import importlib
>>> import rectangle
>>> importlib.reload(rectangle)

然后才能使用 rectangle.py 文件中更新之后的代码。另外一种更简单的重载方法,就是退出当前交互模式,然后依据前述方式重新进入,再引入模块。

如果在 IDE 中编辑 rectangle.py 时,已经退出了交互模式,现在又重新进入交互模式,则不需要上述重加载,直接进入下面的操作(重载之后也执行如下操作)。

代码语言:javascript
复制
>>> from rectangle import *
>>> RightPyramid.__mro__
(<class 'rectangle.RightPyramid'>, <class 'rectangle.Square'>, <class 'rectangle.Rectangle'>, <class 'rectangle.Triangle'>, <class 'object'>)

现在我们看到,MRO 的顺序发生了变化:RightPyramid -> Square -> Rectangle -> Triangle

代码语言:javascript
复制
>>> pyramid = RightPyramid(2, 4)
>>> pyramid.area()
20.0

综上,可以总结一下,在多继承中,如果使用 super() 函数调用父类的属性和方法,务必要了解 MRO 的查找顺序。

对于任何类(或类型),都可以通过 __mro__ 属性查看其“方法解析顺序”——包括但不限于上面的多重继承。例如:

代码语言:javascript
复制
>>> int.__mro__
(<class 'int'>, <class 'object'>)
>>> float.__mro__
(<class 'float'>, <class 'object'>)
>>> str.__mro__
(<class 'str'>, <class 'object'>)
>>> list.__mro__
(<class 'list'>, <class 'object'>)
>>> dict.__mro__
(<class 'dict'>, <class 'object'>)

再观察以上各种类的 MRO,不论是自定义的类还是内置类,都有共同的“祖先” object 类——命名为基类(Base Class)。在下面的操作中会看到一种很有趣的结果:

代码语言:javascript
复制
>>> bool.__mro__
(<class 'bool'>, <class 'int'>, <class 'object'>)

bool 类继承了 int 类,这就揭示了在第3章3.7节曾学到的下述结论的深层原因。

代码语言:javascript
复制
>>> True == 1
True
>>> False == 0
True
>>> 2 + True
3

再回到对 RightPyramid 类的研究上,如果读者仔细观察,会发现它虽然继承了 Triangle 类,实际上并没有通过 super() 函数调用 Triangle 类中的方法。

对于四棱锥的三角形面积,为什么不使用 Triangle 类中的 area() 方法计算呢?当然可以,但是由于在类 RectangleTriangle 中都有 area() 方法,且两个都要在 RightPyramid 中调用,如果还用 super() ,势必造成混乱。于是可以换一种方法,直接用类名称调用对应方法。

代码语言:javascript
复制
class RightPyramid(Triangle, Square):
    def __init__(self, base, slant_height):
        self.base = base
        self.slant_height = slant_height
        Square.__init__(self, self.base)
        Triangle.__init__(self, self.base, self.slant_height)

    def area(self):
        base_area = super(Square, self).area()
        tri_area = Triangle.area(self)
        return tri_area * 4 + base_area

诚然,这也有了“硬编码”的迹象。

如你所见,多继承虽然在编程中很有用,但会导致非常复杂的情况,甚至于让开发者感到困惑。所以,在实践中,用到多继承时都会非常谨慎,尽可能找到其他解决问题的方法,最大程度减少应用多继承的情况。其中,有一种被称为 mixin 的技术形式,深受开发者喜欢。

所谓 mixin (或 mix-in),是 OOP 编程语言中的一个类,它包含供其他类使用的方法。例如:

代码语言:javascript
复制
#coding:utf-8
'''
filename: volume.py
'''
class Rectangle:
    def __init__(self, length, width):
        self.length = length
        self.width = width

    def area(self):
        return self.length * self.width

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

class VolumeMixin:       # (8)
    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):
        return super().area() * 6

if __name__ == '__main__':
    cube = Cube(2)
    print(cube.surface_area())
    print(cube.volume())       # (9)  

程序执行结果:

代码语言:javascript
复制
% python volume.py
24
8

总体上与 rectangle.py 的程序差不多,区别在于注释(8)定义了一个 mixin 类 VolumeMixin ,它的作用即在于向 Cube 类提供 volume 方法,即注释(9)中实例所调用的方法。

通常,mixin 类不会单独使用(如 VolumeMixin ),一个 mixin 类实现一个功能,用于实例化的类继承 mixin 类,可以认为是将若干个功能组装起来,这样使得编程思路清晰,也避免了多继承容易引起的混乱。

为了巩固所学,请读者尝试解决如下问题。

凡是有性生殖的生物体都采用了“多继承”,比如人。人拥有23对不同的染色体,其中有一对染色体决定性别,称为“性染色体”,即 X 染色体和 Y 染色体。女性染色体的组成为 XX,男性染色体的组成为 XY。在自然状态下,夫妇生男生女,就是双方的染色体随机组合的结果。若含 X 染色体的精子与卵子(含有 X 染色体)结合,受精卵性染色体为 XX 型,发育成女胎;若含 Y 染色体的精子与卵子结合,受精卵性染色体为 XY 型,发育成男胎。

将这个生理过程写成一个程序,从而能显示“生男生女”——注意,不是预测。

示例代码如下,供读者参考。

代码语言:javascript
复制
#coding:utf-8
'''
filename: chromosome.py
'''
import random

class Father:
    def __init__(self):
        self.father_chromosome = 'XY'
    
    def do(self):
        print("Make money.")

class Mother:
    def __init__(self):
        self.mother_chromosome = "XX"
    
    def do(self):
        print("Manage money.")

class Child(Father, Mother):
    def child_gender(self):
        fat = random.choice(self.father_chromosome)
        mot = 'X'
        chi = fat + mot
        if "Y" in chi:
            return 1
        return 0

if __name__ == "__main__":
    p = Child()
    if p.child_gender():
        print('is a BOY.')
    else:
        print("is a GIRL.")

自学建议 在第11章11.1节,我们会学到“模块”概念,即每一个 .py 文件。如图8-5-1那样进入交互模式后,可以将自己编写的文件作为模块引入,有利于我们通过“分解动作”理解程序的含义。 如果读者使用的是 Windows 操作系统,也可以打开 cmd,使用 DOS 命令,仿照图8-5-1那样,进入到 .py 文件所在目录,并开启 Python 交互模式。 此外,还可以在 IDE 中实现类似操作,以 VS Code 为例,通过“终端”也能实现类似的操作(如图8-5-2所示)

图8-5-2 在 VS Code 中进入交互模式”

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-06-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 老齐教室 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 8.5.2 多继承
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档