国科大Python编程基础--Chapter09面向对象编程

一、面向对象程序设计方法的由来

1、面向过程的程序设计方法

最早的程序:

  • 目的:用于数学计算
  • 主要工作:设计求解问题的过程
  • 缺点:对于庞大、复杂的程序难以开发和维护

面向过程程序设计基本步骤:

  • 分析程序从输入到输出的各步骤
  • 按照执行过程从前到后编写程序
  • 将高耦合部分封装成模块或函数
  • 输入参数,按照程序执行过程调试

面向过程的程序设计特点:

  • 过程化程序设计的典型方法是“结构化程序设计”方法,是由荷兰学者Dijkstra在70年代提出的。
  • 程序设计原则:自上而下、逐步求精、模块化编程等。
  • 程序结构:
    • 按功能划分为若干个基本模块。
    • 各模块间的关系尽可能简单,功能上相对独立;每一模块内部均是由顺序、选择和循环三种基本结构组成。
    • 其模块化实现的具体方法是使用子程序(过程/函数)。
  • 程序组成:由传递参数的函数集合组成,每个函数处理它的参数,并可能返回某个值。即:主模块+子模块,它们之间以数据作为连接(程序=算法+数据结构)。
  • 程序特点:程序是以过程为中心的。程序员必须基于过程来组织模块。数据处于次要的地位,而过程是关心的重点。
  • 优点:有效地将一个较复杂的程序系统设计任务分解成许多易于控制和处理的子任务,便于开发和维护。

三种程序基本结构

  • 顺序
    在这里插入图片描述
  • 循环
    在这里插入图片描述
  • 选择
    在这里插入图片描述

面向过程的程序设计缺点:

  • 数据与处理数据的方法(函数)相分离。一旦问题(数据)改变,程序员则需要改写或重新编写新的解决方法(功能函数),有时几个关键的数据结构发生变化,将导致整个软件系统的结构崩溃。随着软件规模和复杂性的增长,这种缺陷日益明显。当程序达到一定规模后,为了修改一个小的错误,常可能引出多个大的错误,究其原因,问题就出在传统的程序设计方式上。一般适用于中小型的程序设计及编程应用。
  • 管理的数据类型无法满足需要。当前的软件应用领域已从传统的科学计算和事务处理扩展到了其它的很多方面,如人工智能、计算机辅助设计和辅助制造等,所需处理的数据也已从简单的数字和字符串发展为记录在各种介质上并且有多种格式的多媒体数据,如数字、正文、图形、声音和影像等。数据量和数据类型的空前激增导致了许多程序的规模和复杂性均接近或达到了用结构化程序设计方法无法管理的程度。
  • 可重用性差。只能以函数的方式实现代码重用,效率低,是手工作坊式的编程模式。作为软件公司,都希望设计的程序具有可重用性,即能否建立一些具有已知特性的部件,应用程序通过部件组装即可得到一个新的系统。

例9-1:铅球飞行计算问题 (使用面向过程方法)

  • 功能要求:在给定不同的投掷角度和初始速度下,求解计算铅球的飞行距离。
  • 设计流程(功能分解、逐步求精):
    – 输入:铅球发射角度、初始速度、初始高度、计算的时间间隔
    – 处理:模拟铅球飞行,时刻更新铅球在飞行中的位置
    – 输出:铅球飞行距离
    在这里插入图片描述

简化问题:

  • 忽略空气阻力
  • 重力加速度9.8 m/s2
  • 铅球飞行过程
    • 铅球高度
    • 飞行距离
  • 每隔给定的时间间隔,随时更新铅球在飞行中的位置。
    • 假设起始位置是点(0,0)
    • 垂直方向上运动距离(y轴)
    • 水平方向上移动距离(x轴)

设计参数:

  • 仿真参数:投掷角度angle、初始速度velocity、初始高度height、时间间隔interval
  • 位置参数:x轴坐标xpos,y轴坐标ypos
  • 速度分量:x轴方向分速度xvel,y轴方向分速度yvel

根据提示输入仿真参数:
在这里插入图片描述

计算初始速度:

  • x轴方向的分速度
    • xvel = velocity*cos(theta)
  • y轴方向的分速度
    • yvel= velocity*sin(theta)

完整程序:
在这里插入图片描述

程序模块化:
在这里插入图片描述

2、面向对象程序设计方法

  • 面向对象编程是一种组织程序的新型思维方式;
  • 软件设计的焦点不再是程序的逻辑流程,而是软件或程序中的对象以及对象之间的关系。

面向对象编程的(OOP—Object Oriented Programming)的四个基本机制:

(1)抽象

对具体问题(对象)进行分类概括,提取出这一类对象的共同性质并且加以描述的过程。

涉及:

  • 数据抽象(属性,数据成员)——描述某类对象的属性或状态(对象相互区别的物理量);
  • 代码抽象(方法,函数成员)——描述某类对象的共有的行为特征或具有的功能。

(2)封装

将数据成员和函数成员组合在一起,形成问题的类,然后再根据类来实例化以产生对象。其特点为:

  • 隐蔽对象的内部细节;
  • 对外形成一个边界;
  • 只保留有限的对外接口;
  • 使用方便、安全性好。

(3)继承和派生

  • 根据既有类(基类)派生出新类(派生类)的现象称为类的继承机制,亦称为继承性。
  • 派生类无需重新定义在父类(基类)中已经定义的属性和行为,而是自动地拥有其父类的全部属性与行为。派生类既具有继承下来的属性和行为,又具有自己新定义的属性和行为。当派生类又被它更下层的子类继承时,它继承的及自身定义的属性和行为又被下一级子类继承下去。
  • 意义:
    – 软件复用;
    – 改造、扩展已有类形成新的类。

(4)多态性

  • 多态性是指基类中定义的属性或行为,被派生类继承之后,可以具有不同的数据类型或表现出不同的行为特性,使得同样的消息可以根据发送消息对象的不同而采用多种不同的行为方式。
  • 目的:达到行为标识统一,减少程序中标识符的个数。

3、Python支持的编程方式

  • Python支持面向过程、面向对象、函数式编程等多种编程方式。
  • Python不强制使用任何一种编程方式,可以使用面向过程方式编写任何程序,在编写小程序(少于500行代码)时,不会有问题。但对于中等和大型项目来说,面向对象会带来很多优势。
  • Python对面向对象的语法进行了简化,去掉了面向对象中许多复杂的特性。例如,类的属性和方法的限制符—public、private、protected。
  • Python提倡语法的简单、易用性,这些访问权限靠程序员自觉遵守,而不强制使用。

例9-2:铅球飞行计算问题 (使用面向对象方法)

设计流程:
① 定义出一个“投射体”类——描述投射体类的属性(铅球对象属性:xpos、ypos、xvel、yvel,铅球对象操作:更新投射体状态、返回投射体高度、返回投射体距离);
② 将此类(实例化)具体化以定义出一个对象(代表本次计算的问题);
③ 向此对象发送一条消息——获取投射体的高度。反复进行此步骤,直到投射体高度为零(即已落地);
④ 再向此对象发送一条消息——显示出投射距离。

投射体类定义(保存为一个模块Projectile.py):
在这里插入图片描述

导入模块并应用投射体类定义本次计算的对象:
在这里插入图片描述

程序执行:
• 选手1技术强
• 铅球的出手角度41度
• 出手速度14米/秒
• 初始高度1.8米
• 仿真间隔0.3秒
• 铅球最远飞行距离22.2.米
在这里插入图片描述

• 选手2力量大
• 铅球的出手角度30度
• 出手速度15米/秒
• 初始高度2米
• 仿真间隔0.3秒
• 铅球最远飞行距离23.4米
在这里插入图片描述

二、类和对象

  • 在面向对象程序设计中,程序员可以创建任何新的类型,这些类型可以描述每个对象包含的数据和行为特征,这种类型称为类。
  • 类是一些对象的抽象,隐藏了对象内部复杂的结构和实现。
  • 类由变量和函数两部分构成,类中的变量称为成员变量,类中的函数称为成员函数。

1、对象的概念

  • “万物皆对象”—Python中的所有事物都是以对象形式存在,从简单的数值类型到复杂的代码模块,都是对象。
  • 对象=属性+方法
  • 对象以id作为标识,既包含数据(属性),也包含代码(方法),是某一类具体事物的特殊实例。

对象具有的特点:

  • 类型(一个特定的对象被认为是类型的实例)
  • 内部数据表示(简单或复合)
  • 一组与对象交互的方法(函数)

注意:对象的内部表示是私有的,用户不应当依赖其实现的特定细节。如果直接操作对象的内部表示,可能会损害对象的正确行为。

例如:

num=1234   #int对象
f1=3.14159  #float对象
s1=”Hello”   #str对象
l1=[1, 2, 3, 5, 7, 11, 13]   #list对象
d1={”CA”: ” California”, ” MA”: ” Massachusetts”}               #dict对象

例如:整数1
在这里插入图片描述

例如:字符串’abc’
在这里插入图片描述

例如:列表[1,2,3,4]
在这里插入图片描述

内部数据表示:

  • 一个大小为S(>=L)的对象数组,或
  • 一组独立单元的链接表<data, pointer to next cell>

例如:函数abs
在这里插入图片描述

2、类和对象的区别

  • 类和对象是OOP中两个重要概念。类是对客观世界中事物的抽象,而对象是类实例化后的实体。
  • 类型和变量之间存在着一定的联系,类型是模板,而变量则是具有这种模板的一个实体。同样,有了“类”类型就有其对应的变量实体,这就是对象。
  • 表面上看对象是某个“类”类型的变量,但对象又不是普通的变量,对象是一个数据和操作的封装体。封装的目的就是阻止非法的访问,因此对象实现了信息的隐藏,外部只能通过操作接口访问对象数据。对象是属于某个已知的类的,因此必须先定义类,然后才能定义对象。
  • 要创建新型对象,必须先创建类。类就类似于内置数据类型,可用于创建特定类型的对象。
  • 类指定了对象将包含哪些数据和函数,还指定了对象与其他类的关系。对象封装了数据以及操作这些数据的函数。
  • 一个重要的OOP功能是继承:创建新类时,可让其继承父类的数据和函数。使用好继承可避免重新编写代码,还可让程序更容易理解。
  • 之前介绍的数字、字符串、列表、元组、字典、集合和文件等都是Python提供的内置数据类型(与其他语言不同,Python均定义为类),用他们创建的变量即是相应类型的对象。

类与对象的关系
在这里插入图片描述

例如:自行车类

  • 数据抽象—型号,品牌,换档数
  • 代码抽象—Break(),SpeedUp(),ChangShift(),Run(),Stop()

在这里插入图片描述
例如:按钮(button)
在这里插入图片描述

3、类的定义

  • Python使用class关键字定义一个类,类名首字符一般要大写。
  • 当需要创建的类型不能用简单类型(内置类型)来表示时,则需要定义类,然后利用定义的类创建对象。
  • 格式:
class Class_name(object):

例9-3:创建一个Person类
在这里插入图片描述

说明:

  • 当定义一个类时,如果这个类没有任何父类,则将object设置为它的父类,用这种方式定义的类属于新式类。如果定义的类没有设置任何父类,则这种方式定义的类属于经典类。建议使用新式类,新式类将类与内置类型进行了统一。新式类与经典类在多重继承问题中有一个重要的区别:对于经典类,继承顺序是采用深度优先的搜索算法,对于新式类,继承顺序是采用广度优先的搜索算法。
  • self是指向对象本身的变量,类似于C++的this指针。Python要求,类内定义的每个方法的第一个参数是self,通过实例调用时,该方法才会绑定到该实例上。

4、对象的创建

  • 创建对象的过程称为实例化。当一个对象被创建之后,包含3方面的特性:对象的标识、属性和方法。
  • 对象的标识用于区分不同的对象,当对象被创建之后,该对象会获取一块存储空间,存储空间的地址即为对象的标识。对象的属性和方法与类的成员变量和成员函数相对应。

调用类:

  • 调用类会创建一个对象,(注意括号!)
    obj = <类名>(<参数表>)
  • 返回一个对象实例;
  • 类方法中的self指向这个对象实例!

对象属性和方法的引用:

  • 引用形式
  <对象名>.<属性名>
  <对象名>.<方法名>

例如:

  ’abc’.upper()
  (1+2j).real
  (1+2j).imag

例9-4:对象的创建和应用
在这里插入图片描述

在这里插入图片描述

self示意图:
在这里插入图片描述

  • Python自动给每个对象添加特殊变量self,该变量指向对象本身,让类中的函数能够明确地引用对象的数据。

类中方法引用的等价形式:
<对象>.<方法>(<参数>)

  • 等价于: <类>.<方法>(<对象>, <参数>)
  • 这里的对象就是self

例如:例9-4中,
在这里插入图片描述

说明:第二种格式太繁琐,实际中不使用。

5、对象的显示

  • 在例9-4中,定义了一个方法display,用于显示对象的值。
  • Python还提供了一些特殊的内置方法,能够定制对象的输出。如:
    – 特殊方法__str__,用于生成对象的字符串表示(适合于人阅读的形式);
    – 特殊方法__repr__,返回对象的“官方”表示(适合于解释器读取的形式);此外,可以通过eval()函数重新生成对象。
  • 在大多数类中,方法__repr__都与__str__相同。

例如:
在这里插入图片描述

例9-5:对象的显示
在这里插入图片描述

在这里插入图片描述

说明:

  • 当类中同时存在__repr____str__方法时,使用print输出对象时,自动调用__str__方法;
  • 当类中存在__repr____str__方法之一时,使用print输出对象时,存在哪个方法就自动调用哪个方法。

课堂练习一

三、属性和方法

  • 类由成员变量(对应于对象的属性)和成员函数(对应于对象的方法)组成。属性是对数据的封装,方法则表示对象具有的行为。
  • Python的构造函数、析构函数、私有属性或方法都是通过名称约定区分的。
  • 此外,Python还提供了一些有用的内置方法,简化了类的实现。

1、类的成员属性

  • Python的类的成员一般分为私有属性和公有属性,像C++有定义属性的关键字(public、private、protect),而Python没有这类关键字,默认情况下所有的属性都是“公有的”,对公有属性的访问没有任何限制,且都会被子类继承,也能从子类中进行访问。
  • 若不希望类中的成员在类外被直接访问,就要定义为私有属性。Python使用约定属性名称来划分属性类型。若属性的名字以两个下划线开始,表示私有属性;反之,没有使用双下划线开始的表示公有属性。类的成员变量及成员函数都同样使用这个约定。
  • 另外,Python没有保护类型的修饰符。

2、Python的实例变量和类变量:

  • 实例变量是以self为前缀的成员变量,没有该前缀的成员变量是普通的局部变量。
  • C++中有一类特殊的属性称为静态变量。静态变量能被实例化对象调用,还可以被类直接调用。当创建新的实例化对象后,静态变量并不会获取新的内存空间,而是使用类创建的内存空间。因此,静态变量能够被多个实例化对象共享。
  • 在Python中静态变量称为类变量,类变量可以被该类的所有实例共享,但并不是无条件的。
  • 对类变量的访问,既可以通过类名,也可以通过对象名访问。即:类名/对象名.类变量

例9-6:实例变量和类变量
在这里插入图片描述
在这里插入图片描述

Python类变量的说明:

  • Python的类变量与C++的静态变量有所不同,并不是无条件地由所有对象共享;
  • 包含类变量的类,一旦创建对象之后,会将当前类变量拷贝一份给该对象,当前类变量的值是多少,该对象得到的类变量值就是多少,如果通过对象改变类变量的值,不会影响通过类名引用的类变量值,也不会影响其他实例对象的类变量的值,因为各对象均有各自的副本,更不会影响类本身拥有的那个类变量的值,只有类自己才能改变类变量本身的值;
  • 另外,在没有通过对象改变类变量值时,通过类改变类变量值,是能够影响对象引用的类变量的值的。
  • 因此,若想达到C++静态变量的效果,就不要通过对象修改类变量的值。

例如(接上例):
在这里插入图片描述

在这里插入图片描述

关于Python私有属性的访问:

  • 类的私有属性是指在类内可以直接访问,而类外不能直接访问的属性。
  • 但Python为了程序测试和调试方便,提供了直接访问私有属性的方法。
  • 私有属性访问的格式:instance._classname__attribute
  • 说明:
    – instance表示实例化对象;
    – classname表示类名;
    – attribute表示私有属性
  • 注意:classname之前是单下划线,attribute之前是双下划线。

例9-7:访问私有属性
在这里插入图片描述
在这里插入图片描述

注意:

  • Python对类的属性和方法的定义次序并没有要求。合理的方式是将类属性定义在类中最前面,然后再定义私有方法,最后定义公有方法。
  • Python的类还提供了一些内置属性,用于管理类的内部关系。例如:__dict____bases____doc__等。

例9-8:常见的类内置属性用法
在这里插入图片描述
在这里插入图片描述

3、类的方法

  • 类中的方法实际上就是执行某种数据处理功能的函数。
  • 与普通函数定义一样,类中的方法在定义时也需要使用def 关键字。
  • 类中的方法分为两类:普通方法和内置方法。
    – 普通方法需要通过类的实例对象根据方法名调用;
    – 内置方法是在特定情况下由系统自动执行。

类的普通方法的定义和调用:

  • 在定义类的普通方法时,要求第一个参数需要对应调用方法时所使用的实例对象(一般命名为self,但也可以改为其他名字)。
  • 当使用一个实例对象调用类的普通方法时,其语法格式为:
    实例对象名.方法名(实参列表)
  • 提示:在通过类的实例对象调用类中的普通方法时,并不需要传入self参数的值,self会自动对应调用该方法时所使用的对象。

类的方法的属性:

  • 类的方法也分为公有方法和私有方法。
  • 私有方法不能被模块外的类或方法调用,私有方法也不能被外部的类或函数调用。

类的静态方法:

  • C++中的静态方法使用关键字static声明,而Python使用函数staticmethod()或@staticmethod修饰器将普通的函数转换为静态方法。
  • Python的静态方法并没有和类的实例进行名称绑定,要调用除了使用通常的方法(通过对象名调用),使用类名作为其前缀亦可。
  • 类的静态方法通常用来访问类变量。

例9-9:类的普通方法及静态方法的使用
在这里插入图片描述
在这里插入图片描述

4、__init__方法

  • 构造函数是类的内置方法,用于初始化类的内部状态,为类的属性设置默认值。
  • C++的构造函数是与类同名的方法,而Python的构造函数名为__init__
  • __init__方法一般用于完成对象数据成员设置初值或进行其他必要的初始化工作。
  • __init__方法是可选的,若不提供__init__方法,Python将会给出1个默认的__init__方法。

初始化过程:

  • 当类被调用后,Python将创建实例对象
  • 创建完对象之后,Python自动调用的第一个方法为__init__()
  • 实例对象作为方法的第一个参数(self)被传递进去,调用类创建实例对象时的参数都传给__init__()

例9-10:构造函数应用一
在这里插入图片描述

在这里插入图片描述

例9-11:构造函数应用二
上例中,若给出的值不合理也无法限制。通常的做法是在类中定义设置函数和获取函数。
在这里插入图片描述
在这里插入图片描述

5、__del__方法

  • 析构函数用于销毁对象,释放对象占用的资源;
  • Python提供了析构函数__del__();
  • 析构函数也是可选的。若程序中不提供析构函数,Python会提供默认的析构函数。

说明:类对象销毁有如下三种情况

  • 局部变量的作用域结束。
  • 使用del删除对象。
  • 程序结束时,程序中的所有对象都将被销毁(取决于所用的集成开发环境)。
  • 注意:
    – 如果多个变量对应同一片内存空间,则只有这些变量都删除后才会销毁这片内存空间中所保存的对象,也才会自动执行析构方法。

例9-12:析构函数应用一
在这里插入图片描述

在这里插入图片描述

例9-13:析构函数应用二
在这里插入图片描述

  • PyCharm IDE执行
    在这里插入图片描述
  • IDEL IDE执行
    在这里插入图片描述

说明:

  • 当对象不再被使用时,__del__方法自动被调用,但是在有些Python环境下很难保证这个方法被自动调用,若想指明它的运行,就要显式地调用析构函数:del 对象名
  • 由于Python中定义了__del__()的实例对象将无法被Python的循环垃圾收集器(gc)收集,所以建议只有需要时才定义__del__
  • 事实上,使用Python编写程序可以不考虑后台的内存管理,直接面对程序的逻辑。

6、类的内置方法

  • Python类定义了一些专用的内置方法,也有称为特殊方法或魔术方法。这些内置方法丰富了程序设计的功能,用于不同的应用场合,使得许多用于内置数据类型的运算符或函数可以用于自定义类类型的对象。
  • 所有特殊方法的名称以两个下划线(__)开始和结束。
  • 之前介绍的__init____del____str____repr__都是类的内置方法。表9-1列出了类常用的其他内置方法。

表9-1 类常用的内置方法
在这里插入图片描述

在这里插入图片描述

例9-14:获取对象的属性为序列中的各个元素
如果类中将某个属性定义为序列,可以使用__getitem__()输出序列属性中的各个元素。
在这里插入图片描述

在这里插入图片描述

例9-15:对“+”和“>”进行重载
在这里插入图片描述

说明:

  • 运算符用于表达式的计算,而对于自定义类型的对象则不能直接用其计算。运算符的重载可以实现对象之间的运算。
  • Python可将运算符和类的内置方法关联起来,每个运算符都对应1个内置函数。

主调函数及执行结果:
在这里插入图片描述

在这里插入图片描述

例9-16:定义迭代器类及应用
在这里插入图片描述

在这里插入图片描述

7、属性和方法的动态特性

  • Python作为动态脚本语言,编写的程序具有很强的动态性。
    – 可以动态地添加新的属性(在类外通过类名或对象名赋值即可实现)
    – 可以动态添加类的方法
    – 还可以对已定义的方法进行修改

  • 添加新方法的语法格式为:
    class_name.method_name=function_name

  • 其中,class_name表示类名,method_name表示新的方法名,function_name表示1个已经存在的函数。

  • 修改方法的语法格式为:
    class_name.method_name=function_name

  • 其中,class_name表示类名,method_name表示已经存在的方法名,function_name表示1个已经存在的函数,该赋值表达式表示将函数的内容更新到方法。

说明:

  • 通过类名动态添加或修改新属性和新方法时,该属性和方法属于该类所有对象,且function_name的第一个参数为self;
  • 通过对象动态添加或修改新属性和新方法时,该属性和方法只属于该对象,此时function_name的参数就不能有self了。

例9-17:向类中动态地添加新方法并添加新属性(通过类名添加的新方法及属性属于类)
在这里插入图片描述

在这里插入图片描述

例9-18:动态修改类中已有的方法并添加新属性(通过对象添加的新方法及属性属于对象)
在这里插入图片描述

在这里插入图片描述

例9-19:创建一个表示整数集合的新类

  • 创建一个新类表示整数集合
    – 初始时集合为空
    – 每个特定的整数只能在集合中出现一次(注:必须在方法中强制实现)
  • 内部数据表示
    – 用一个列表存储集合中的元素
  • 接口
    – insert(e):若整数e不存在,则插入e到集合中
    – member(e):若整数e在集合中返回True,否则返回False
    – remove(e):从集合中删除整数e,若不存在则报错

程序实现:
在这里插入图片描述
在这里插入图片描述

执行:
在这里插入图片描述

例9-20:创建一个关于人的信息的类及相关方法

  • Person:name,birthday
    – Get last name
    – Get age
    – Sort by last name
    在这里插入图片描述

执行:
在这里插入图片描述

• 课堂练习二

四、继承

  • 继承是面向对象的重要特性之一。继承可以重用已经存在的数据和行为,减少代码的重复编写,从而实现代码的重用。
  • 通过继承可以创建新类,原始的类称为基类、父类或超类,新类称为子类或派生类。
  • 子类会继承父类中定义的所有属性和方法(公有的),另外也能够在子类中增加新的属性和方法。
  • 如果一个子类只有一个父类,则将这种继承关系称为单继承;如果一个子类有两个或更多父类,则将这种继承关系称为多重继承。

1、子类(派生类)的构成

  • 吸收基类成员
    – 默认情况下派生类包含了全部基类中的所有成员(不包括构造函数和析构函数,这两个函数单独处理)
  • 改造基类成员
    – 如果派生类声明了一个和某基类成员同名的新成员,派生的新成员就隐藏或覆盖了外层同名成员
  • 添加新的成员
    – 派生类增加新成员使派生类在功能上有所扩展

2、子类的定义

  • 定义子类时需要指定父类,其语法格式为:
 class 子类名(父类名1, 父类名2,, 父类名M):
       语句1
       语句2
        …
       语句N
  • 当M等于1时,则为单继承;当M大于1时,则为多重继承。

3、继承关系的表示

  • 当类设计完成之后,就可以考虑类之间的逻辑关系。类之间存在继承、组合、依赖等关系,可以采用UML工具表示类之间的关系。
  • 例如,有两个子类Apple、Banana继承自父类Fruit,父类中有1个公有实例变量和1个公有的方法。图9-1表示了Fruit类和Apple、Banana类之间的继承关系(公有实例变量和方法假定用“+”表示,私有的用“-”表示)。
  • Apple、Banana类可以继承Fruit类的实例变量color和方法grow()。

图9-1:类的继承关系
在这里插入图片描述

4、单继承关系中的构造函数

  • 如果子类没有自己的构造函数,则会直接从父类继承构造函数。
  • 如果子类有自己的构造函数,不会自动调用父类的构造函数:
    – 如果需要用到父类的构造函数,则需要在子类的构造函数中显式地调用;
    – 如果子类需要扩展父类的行为,可以添加__init__方法的参数。

例9-21:类的继承
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

例9-22:在例9-20一般人的基础上增加MIT人员的内容

  • Person: name, birthday
    – Get last name
    – Get age
    – Sort by last name
  • MITPerson: Person + ID Number
    – Assign ID numbers in sequence
    – Get ID number
    – Sort by ID number

定义MIT人员的类:
在这里插入图片描述

执行:
在这里插入图片描述

说明:

  • 为什么P4<P1能够执行,而P1<P4不行?
    – P4<P1相当于调用P4.__lt__(P1),相当于使用与P4对象相关的Person的方法,根据人的姓名做比较,因此可正常执行;
    – P1<P4相当于调用P1.__lt__(P4),相当于使用与P1对象相关的MITPeron的方法,根据人的IDNum做比较,而P4是一个Person类对象,没有IDNum,因此无法比较。

例9-23:在例9-22的MITPerson基础上,再派生出学生和研究生(多层/多级派生)

  • Person: name, birthday
    – Get last name
    – Get age
    – Sort by last name
  • MITPerson: Person + ID Number
    – Assign ID numbers in sequence
    – Get ID number
    – Sort by ID number
  • Students: several types, all MITPerson
    – Undergraduate student: has class year
    – Graduate student
  • 创建一个覆盖所有学生的超类较好:
    在这里插入图片描述

定义Student类及其子类:
在这里插入图片描述

在这里插入图片描述

例9-24:创建一个mylist类,继承自内置数据类型list(列表)

  • Python除了可以从自定义类派生新类,还可以从内置数据类型派生新类。
  • 本例派生类mylist增加一个方法“累乘”(product),返回所有数据项的乘积。

例9-24源程序:
在这里插入图片描述
在这里插入图片描述

5、多重继承

  • Python支持多重继承,即一个类可以继承多个父类。
  • 多重继承的语法格式:
    class_name(parent_class1, parent_class2…)
  • 其中class_name是类名,parent_class1和parent_class2…是父类名。

类的多重继承关系:

  • 例如:西瓜既具有水果的特性,又具有蔬菜的特性。水果和蔬菜可以作为西瓜的父类。
  • 右图表示了Watermenlon类和Fruit、Vegetable类之间的多重继承关系。
    在这里插入图片描述

多重继承关系中的构造函数:

  • 子类从多个父类派生,而子类又没有自己的构造函数时,
    (1)按顺序继承,哪个父类在最前面且它又有自己的构造函数,就继承它的构造函数;
    (2)如果最前面第一个父类没有构造函数,则继承第2个的构造函数,第2个没有的话,再往后找,以此类推。

例9-25:多重继承应用
在这里插入图片描述

在这里插入图片描述

  • 说明:由于Watermelon继承了Vegetable、Fruit类,因此Watermelon将继承__init__()。但是Watermelon只会调用第1个被继承的类的__init__,即Vegetable类的__init__()

6、抽象基类

  • 抽象基类是对一类事物的特征行为的抽象,由抽象方法组成。
  • 定义方式:Python3中,通过abc模块中的元类ABCmeta定义抽象基类,通过修饰器@abstractmethod定义抽象方法;
  • 抽象基类不能被直接实例化。

抽象基类的作用:

  • 抽象类为抽象和设计的目的而声明;
  • 将有关的数据和行为组织在一个继承层次结构中,保证派生类具体要求的行为;
  • 对于暂时无法实现的函数,可以声明为抽象方法,留给派生类去实现。

例如:

  • 例9-25中的Apple、Banana都继承了Fruit类,Apple、Banana类都具有父类的grow()方法。Fruit类是对水果的抽象,不同的水果有不同的培育方法,因此生长的情况也不相同。Fruit类的grow()是对所有水果行为的抽象,并不知道如何生长,因此,grow()方法应该是一个空方法,即抽象方法。

例9-26:抽象基类应用
在这里插入图片描述
在这里插入图片描述

五、多态性

  • 继承机制说明子类具有父类的公有属性和方法,而且子类可以扩展自身的功能,添加新的属性和方法。
  • 子类还可以对从父类中继承过来的方法进行重新定义,从而使得子类对象可以表现出与父类对象不同的行为。
  • 因此,子类可以替代父类对象,这种特性称为多态性。即指在执行同样代码的情况下,系统会根据对象实际所属的类去调用相应类中的方法。
  • 从根本上说,所谓多态性是指当不同的对象收到相同的消息时,产生不同的动作。

例9-27:多态性应用

  • 米老鼠和唐老鸭都是动物,都可以从动物类继承而来,动物都可以跑(假设),都可以给它们发出run的指令,但它们跑起来显然是不一样的。

程序及执行结果:
在这里插入图片描述

在这里插入图片描述

说明:

  • 从本例可以了解到多态的好处,即当需要传入DonaldDuck、MickeyMouse或其他动物时,只需要接收Animal类型就可以了,因为它们都是Animal类型,然后按照Animal类型进行操作即可。由于Animal类型有run()方法,因此,传入的任意类型,只要是Animal类或者子类,就会自动调用实际类型的run()方法。这就是多态的含义。
  • 调用方只管调用,不管细节,而当新增一种Animal的子类时,只要确保run()方法编写正确,不用管原来的代码是如何调用的。这就是著名的“开闭”原则:对扩展开放,允许新增Animal子类;对修改封闭,不需要修改依赖Animal类型的AnimalRun()等函数。

例9-28:多态性应用(一个简单游戏)

  • 创建一个名为Undercut的简单游戏。在这个游戏中,两个玩家同时选择1-10的整数,若一个玩家选择的整数比对方选择的整数小1,则该玩家获胜,否则算打平手。
  • 例如,若Thomas和Bonnie一起玩游戏Undercut,且他们选择的数字分别为9和10,则Thomas获胜;如果他们分别选择4和7,则打成平手。
  • 基类定义:
    在这里插入图片描述

玩Undercut游戏的函数为:
在这里插入图片描述

实现get_move函数:

  • 虽然在游戏Undercut中,走法不过是选择1—10的数字,但人和计算机选择数字的方式不同。人类玩家通过键盘输入一个1—10的数字,而计算机玩家使用函数来选择数字。因此,Human和Computer类需要专用的get_move(self)方法。
  • Computer类和Human类的get_move方法
    在这里插入图片描述

完整程序:
在这里插入图片描述

在这里插入图片描述

人和计算机玩这款游戏:
在这里插入图片描述

两个计算机玩家玩儿play_undercut:

在这里插入图片描述

两个人类玩家玩儿play_undercut:
在这里插入图片描述

本例中人对人、人对计算机、计算机对计算机玩游戏也充分展示了多态的威力:使用相同的函数实现了截然不同的行为。我们没有编写三个不同的函数,而是只编写一个函数,并给它传递不同的对象。


整理不易🚀🚀,关注和收藏后拿走📌📌欢迎留言🧐👋📣✨
快来关注我的公众号🔎AdaCoding 和 GitHub🔎 AdaCoding123
在这里插入图片描述

  • 27
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值