第5章 面向对象编程基础
5.1 面向对象的概念与特征
5.1.1 面向对象的概念
- 面向对象
-
对象是程序的基本单元,程序是各种即有独立性又相互调用的对象构成
-
编程范式
- 过程式
- 将程序看作一系列指令和函数的集合,根据一定的流程控制顺序来执行程序指令
- 与面向对象相容
-
函数式
- 将待解决问题解构成一系列函数
- 与面向对象不相容
-
问题解构的角度来看
- 面向对象编程是对现实世界的模拟,对象就是现实世界中的个体
- 每个对象都具有自已的属性,还能够作出各种行为,对象之间还能相互作用
- 例如,一个城市的交通系统中有很多的行人、车辆、信号灯等构成
- 从程序设计的角度来看
- 对象其实是一些“变量”和一些“函数”的组合体,是为了实现特定的程序功能不可或缺的组成部分
- “变量”称为__属性__
-
“函数”称为__方法__
-
面向对象编程的优点
- 模块化程度高
- 能够从更加抽象的层次去解构问题、分解任务
- 便于代码重用
- 够方便地利用一种对象的功能实现另一种类型的对象
- 继承性
- 封装性良好
- 属性被封装到对象之中,对象在被调用时其内部实现过程对于调用者来说是透明的
- 封装性
-
灵活性高
- 不同的类形的对象是能够相互替代的,对象能够以不同的身份参与到不同的功能调用之中
- 多态性
-
面向对象编程的现状
- Simula:第一种具有面向对象特征的编程语言是
- Smalltalk:一种完全面向对象的编程语言
- 面向对象的思想影响了一大批编程语言
5.1.2 类与对象
- 抽象
- 从一类事物中归纳、提取出它们共同的、本质的特征,忽略与关注目标无关的、非本质特征,最终形成一个能够用于描述这类事物的概念
- 类
- 抽象出的概念称为类
- 具有普遍性、抽象性
- 对象
- 对象是类的实例
-
个性化、具体的
-
例
定义类:
1 2 3 4 | |
1 2 3 | |
5.1.3 封装性
- 封装性的含义
- 完成一类对象功能所需的数据及对数据的操作方法被打包为一个整体
- 对外隐藏不必要的细节
- 类成员的访问具有一定的权力限制
5.1.4 继承性
- 继承(Inheritance)
- 是一种创建新类的方法,能够有效地实现代码重用
-
基类(父类)与派生类(子类)
-
例
1 2 3 4 5 6 7 | |
5.1.5 多态性
- 特征
- 派生类对象由于具有父类对象的所有功能,因而可以作为父类对象使用
- 例如,
汽车类的对象能够行驶在机动车道上,客车类是汽车类的子类,其对象也能够行驶在机动车道上
- 例如,
-
在被调用时每个对象可能以不同的方式做出响应
- 例如,跑车的行驶与普通汽车有着显著的差异,因此
跑车类需要重新定义行驶方法 - 相同的指令,
汽车对象与跑车对象会调用各自的方法做出不同的响应
- 例如,跑车的行驶与普通汽车有着显著的差异,因此
-
注意
- 子类对象不会因为被当作父类对象使用就变成了父类对象
客车虽然可以被当作汽车来使用,但是它本质上是客车,不会变成普通汽车
- 子类对象被作为父类对象使用时,只能调用父类对象拥有的属性
- 当
客车对象被作为汽车使用时,我们无法确定它究竟是什么,它可能是货车也可能是跑车,因而不能访问最大载客量变量或调用上下客方法
- 当
- 子类重写了父类方法时,即使子类对象被当作父类对象使用,调用的也是子类中的方法
- 一个
跑车对象不论被看作汽车还是跑车,当行驶时被调用的只能是跑车中的行驶方法
- 一个
5.2 类的定义与实例化
5.2.1 类的定义
1 2 3 | |
1 2 3 4 5 | |
- 定义属性和方法
- 属性:与普通变量定义一致
- 方法
1 2 3 | |
self
- 对象自身
- 通过self来访问对象中包含的其他属性或方法
1 2 3 4 5 | |
self参数的名称可以任意
5.2.2 类的实例化
- 实例化
1 2 3 4 | |
1 | |
- 实例化的过程
- 由两个特殊方法
__new__和__init__实现- 如果用户没有定义
__new__或__init__默认会调用父类的__new__或__init__方法
- 如果用户没有定义
1 2 3 4 5 6 7 8 | |
1 | |
1 2 | |
- 实例化过程中参数的传递
1 2 3 4 5 6 7 8 9 10 | |
1 | |
1 2 3 4 | |
1 2 3 4 5 6 | |
1 2 | |
1 | |
5.2.3 类成员的隐藏
- 所有名称以
__开头的属性为隐藏成员
1 2 3 4 5 6 7 | |
1 2 | |
1 | |
1 | |
1 2 3 4 5 6 7 8 9 | |
1 | |
1 2 3 4 5 6 7 8 9 | |
- Python中没有真正的“私有”
1 | |
1 | |
1 | |
1 | |
- 一种约定
- 以
_开头的为隐藏成员
5.2.4 类命名空间 *
- Python中的命名空间
- 内建命名空间:Python解释器启动时创建的命名空间;
- 全局命名空间:加载模块时创建的命名空间;
- 闭包命名空间:定义闭包时创建的命名空间;
- 局部命名空间:定义函数或类时创建的命名空间。
1 | |
1 | |
1 | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | |
- 类命名空间
- 当定义一个类之后,就产生了一个命名空间
1 2 3 4 5 6 7 8 | |
1 | |
5.3 进一步了解属性
5.3.1 类属性与实例属性
- 定义在类命名空间中的属性称为类属性或静态属性
- 类的所有对象共享相同的类属性
- 类属性对于类内的方法来说类似于全局变量。当在方法中试图修改类属性的取值时,相当于重新定义了局部变量,而局部变量会覆盖掉类命名空间中的同名变量。
- 定义在
self对象中的属性称为实例属性 - 每个对象自有
1 2 | |
1 2 3 4 | |
1 | |
1 2 3 | |
1 2 3 4 | |
1 | |
5.3.2 property装饰器
- 直接访问类属性
1 2 3 | |
1 2 | |
1 | |
- 间接访问类属性
1 2 3 4 5 6 7 8 9 | |
1 2 | |
1 | |
property装饰器能够将方法转换为属性的形式来使用
1 2 3 4 5 6 7 | |
1 2 | |
1 | |
- 完整的
property
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
1 2 | |
1 | |
1 2 | |
1 | |
1 2 | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
5.4 进一步了解方法
5.4.1 实例方法、类方法与静态方法
实例方法
- 类中定义的普通方法第一个形参是self,表示对象自身。可以利用self来访问对象 的其他属性或方法,也可以在其他方法中使用self来访问当前方法。这类方法称为实例方法。
类方法
- 用
classmethod装饰器实现 - 第一个参数名常为
cls - 可以通过类直接调用,也可以通过对象调用
1 2 3 4 5 6 | |
1 | |
1 | |
1 2 | |
1 | |
静态方法
- 用
staticmethod装饰器实现 - 无特殊参数
- 可以通过类直接调用,也可以通过对象调用
1 2 3 4 | |
1 | |
1 | |
1 2 | |
1 | |
5.4.2 方法重载*
- 用
singledispatchmethod装饰器实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
5.5 类的继承
5.5.1 派生类的定义
- 语法形式
1 2 | |
- 默认的基类为
object - 所有的类都是
object类的派生类 object类中定义了很多特殊属性和特殊方法,子类重写这些方法能够对类进行定制以实现特殊的功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
5.5.2 方法的重写
- 在派生类中重新定义了基类中的属性或方法时,基类中的同名属性或方法会被覆盖掉,称为属性或方法的重写
- 基类的隐藏成员在派生类中也是不可见的
- 基类中改变隐藏方法名称的原因
重写普通方法
1 2 3 | |
1 2 | |
1 | |
重写__init__
- 需在派生类的
__init__方法中调用基类的__init__方法来初始化
1 2 3 4 5 6 7 8 9 | |
重写property*
- 被
property修饰的方法以及相应的setter方法和deleter方法是一个整体 - 如果派生类中要重写基类中定义的
property,不能仅重写被property修饰的方法,还需要将setter方法和deleter方法也重,才能保证派生类中property的完整性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | |
1 2 | |
1 | |
1 | |
1 2 3 4 5 6 7 8 9 | |
- 重写
property中的方法其中之一
1 2 3 4 5 6 7 8 | |
1 2 3 | |
1 | |
5.5.3 多重继承*
- 一个派生类继承自多个基类
- 多重继承中会出现命名冲突问题,增加了程序的复杂性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | |
- 方法解析顺序(Method Resolution Order,MRO)
- 解释器依据该顺序进行搜索,执行找到的第一个方法
1 | |
1 | |
5.5.4 对象、类的关系
关系检查
isinstance- 检查一个对象是否是一个类的实例
1 2 | |
1 | |
1 | |
1 | |
issubclass- 判断一个类是否另一个类的派生类
1 | |
1 | |
1 | |
1 | |
1 | |
1 | |
鸭子类型和协议
- 动态语言特有的一种特征,其含义是对象方法要比其类型更重要
- 为程序设计带来了很大的灵活性
- 例:
- 定义一个驾驶员类
Driver
1 2 3 4 5 6 | |
1 2 3 | |
1 | |
- 蹦蹦车类
Bouncy并不是Moto类的派生类
1 2 3 | |
1 2 3 | |
1 | |
-
Bouncy的实例虽然不是Moto的实例,但是它们的行为非常相似。这种情况被称为“鸭子类型” -
If it walks like a duck and it quacks like a duck, then it must be a duck
-
协议
- 一组约定的用 于实现特定功能的方法
- 如果一个类中实现了一个协议中的所有方法,就可以说该类实 现了这个协议,相应地这个类的对象就具备了协议中约定的功能
- 协议是一种在文档层面的非正式约定,不具有强制性,在语言层面上不 会感知到协议的存在
- 动态语言中的协议与静态语言(如 Java 和 C++)中接口的概念具有相似的作用
5.5.5 调用基类方法
通过基类名调用基类方法
Wagon、Coach、Pickup等类的定义中使用了这种方法在__init__中调用基类的__init__方法来初始化基类属性- 缺点
- 增加了代码之间的耦合性
- 在多重继承的情况下会产生重复调用的问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | |
1 | |
1 2 3 4 5 | |
通过super调用基类方法
super- 于创建代理对象或代理类的类
- 代理对象或代理类会按照方法解析顺序(MRO)来查找要执行的方法
- 调用方式
super(TYPE, OBJ):返回一个代理对象。TYPE是一个类,用于指定MRO中的查找开始位置(不含TYPE自身),OBJ用于指定查找哪个类的MRO。OBJ必须是TYPE的实例,即isinstance(OBJ, TYPE)为True。super():这种方式只能用于类内部,与super(TYPE, OBJ)一致,只不过TYPE和OBJ分别传入的是当前所属的类以及所在方法中的self对象。super(TYPE1, TYPE2):返回一个代理类。TYPE1和TYPE2都是类,TYPE1用于指定MRO中的查找开始位置(不含TYPE1自身),TYPE2用于指定查找哪个类的MRO。由于返回的是代理类,所以在调用方法的时候必须考虑方法的时候必须手动传入特殊参数self。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | |
1 | |
1 2 3 4 | |
5.6 混入*
5.6.1 混入的概念
- 混入(MinxIn)是有别于继承的另一种代码重用机制,它从另一个角度对现实世界 中事物的关系进行组织和划分,是继承机制的一种有效的补充
- 混入机制通常利用多重 继承机制实现,实际上混入也是多重继承最主要的应用方式
继承与混入的区别
- 继承
- 混入
- “载货能力”和“载客能力”被称为混入类,“汽 车”类称为具体类,“微卡”、“轿车”等称为被混入类
-
混入类与被混 入类之间并不是父子关系
-
混入的优势
- 混入与继承相比一个重要的优势是能够有效降低代码的复杂程度
-
在继承关系中,继承的层次会随着新特征的加入而增加,产生冗余的代码,导致程序开发成本和维护成本增加。混入机 制就能够很好地解决这个问题。
-
混入类应当具备如下特征
- 与具体类之间不存在逻辑上的依赖关系
- 包含了一组联系紧密的方法以实现特定的功能
- 不应该被实例化,混入类的对象没有存在的意义
- 继承与混入的相同之处在于它们都包含了一组用于实现特定功能的方法;不同之处在于协议仅仅是一种不具有强制性的约定,而混入类中 包含的是具体的、已经被实现了的方法。
5.6.2 Python 中的混入
混入的实现方式
- Python中混入是用多重继承实现的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
1 2 | |
1 | |
1 | |
1 | |
1 2 | |
1 | |
1 | |
1 | |
在混入类中调用具体类的方法
- 混入类中定义的功能具有通用性,可以与多种具体类结合个性化地实现其中定义的功能
- 例如,
Car和Bus混入CoachMixIn之后都具备了载客功能,但是它们的载客功能差异很大 - 载客功能的实现明显不是混入类
CoachMixIn独立实现的,还需要依赖具体类Moto - 混入类中一般没有状态信息,必须依赖具体类才能真正实现功能
- 混入类不应当被实例化的真正原因
- 混入类中如何调用具体类的方法?
- 从多重继承的语法来说,混入类和具体类之间是并列的,它们之间没有任何关系
-
Python 的super和方法解析顺序实现
-
super根据参数来确定在 MRO 中的查找方式,其参数传递形式有三种: super(TYPE, OBJ):返回一个代理对象。其中TYPE是一个类,用于指定 MRO 中的 查找开始位置(不含TYPE自身)。OBJ用于指定查找哪个类的 MRO,必须是TYPE的 实例,即isinstance(OBJ, TYPE)必须为True。super():这种方式只能用于类内部,与super(TYPE, OBJ)一致,只不过省去了显 式传入的实参TYPE和OBJ。解释器默认传入的实参是当前所属的类以及所在方法中 的self对象。-
super(TYPE1, TYPE2):返回一个代理类,其中TYPE1和TYPE2都是类名,TYPE1用 于指定 MRO 中的查找开始位置(不含TYPE1自身),TYPE2用于指定查找哪个类的 MRO。由于返回的是代理类而不是代理对象,因此在调用方法的时候必须手动传入特殊参数self。 -
super能够调用基类的__init__方法的原因 -
以类
Wagon为例,super()与super(Wagon, self)等价,要查找的是由self确定的类Wagon的 MRO, 从该 MRO 中类Wagon之后开始查找。Wagon之后的第一个类就是Wagon的基类Moto,因 此super().__init__查找到的就是Moto的__init__方法 -
例:在混入类中调用具体类的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
1 2 | |
1 | |
1 | |
1 | |
-
基类列表中混入类必须位于具体类之前
-
例:利用混入扩展类的功能
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
1 2 3 | |
1 | |
1 | |
1 | |
- 混入类使用原则
- 混入类使用特殊的命名,一般以MinxIn为后缀
- 混入类只应当实现功能,不应当有状态信息
- 不同混入类尽量不要包含名称相同的方法
- 在定义被混入类时,基类中可以包含多个混入类,但只应当有一个具体类,并且混入类都要位于具体类之前
本页面的全部内容在 生信资料 bio.0594codes.cn 和 莆田青少年编程俱乐部 0594codes.cn 协议之条款下提供,附加条款亦可能应用