21. 面向对象及特性
Hi,大家好。我是茶桁。
今天开始,我们要迈向 Python 的另外一个台阶了,那就是面向对象。
面向对象编程(Object Oriented Programming),简称为 OOP,是一种以对象为中心的程序设计思想。
与之相对的,就是面向过程编程(Procedure Oriented Programming), 简称为 POP, 是一种以过程为中心的程序设计思想。
面向对象和面向过程
接下来,让我们先了解一下这两个编程思想到底有什么不同。还记得咱们之前讲过宋丹丹老师小品里的经典的「把大象装进冰箱分几步」吗?小品给出的答案是三步对吧?
- 第一步:打开冰箱门
- 第二步:把大象装进去
- 第三步:关上冰箱门
设计思想的不同
那么这个答案,就是一种面向过程的思维,遇到问题之后,分析解决问题的步骤,然后一步步的去实现。
那么如果是面向对象的话,又该如何去做?
面向对象是通过分析问题中需要的抽象模型,然后根据需要的功能分别去创建模型对象,最终由模型对象来完成程序。那这个「把大象装进冰箱分几步」的问题我们该如何去考虑呢?
首先,面向对象要解决这个问题,需要先建立出抽象模型,比如:
- 打开冰箱门和关闭冰箱门,这都属于一个冰箱的功能,
- 大象走进去,这就是大象的功能。
- 到此时我们就出现了两个抽象模型,一个是冰箱,一个是大象。
冰箱具有 打开和关闭的功能,大象具有走路的能力。
分析到这里,就是面向对象的思想,具体完成的话,就是去创建冰箱和大象这两个对象,最终完成这个程序
冰箱对象-开门,大象对象-走进冰箱,冰箱对象-关门
这个问题解决了,我们再来思考一个新的问题:「想吃清蒸鱼怎么办?」
当然是按照做菜的顺序一步一步来对吧?这就是典型的面向过程思维:
- 买鱼,买料
- 杀鱼和清理,并且腌制
- 锅里烧水
- 把鱼放进去,开始蒸鱼。
- 十分钟后开盖,把鱼端出来,然后浇汁。
这样,一步一步的完成这个愿望,就是面向过程所作的事情。
轮到面向对象,又该如何呢?
- 需要一个对象:大厨。
- 告诉大厨,我想吃清蒸鱼。
那么大厨呢,有可能是我们自己训练的,也有可能是其他五星酒店挖过来的。不管如何,这是一个已经完善建立好的对象,我们直接拿来用就可以了。面向对象呢,就是这样寻找具体的对象去解决问题。对于我们来说,调用了对象,而对象完成了这个过程。
当然,具体大厨这个对象里肯定还是一步一步的去完成过程,也就是说,最终面向对象中是由面向过程的体现的。但是思维方式,也就是设计思想是完全不同的。
优缺点
既然有不同之处,那必然是有优缺点的。因为有对比嘛。面向过程有其优点,当然,面向对象也有其缺点。
面向过程的核心是过程,过程就是指几觉问题的步骤。其优缺点非常明显:
- 优点: 将负责的问题流程化,进而实现简答。
- 缺点: 扩展性差(更新、维护、迭代)
而面向对象的核心是对象,是一个特征和功能的综合体,其优缺点如下:
- 优点:可扩展性高
- 缺点:编程复杂度相对面向过程高一些,这里的复杂度指的是计算机在执行面向对象的程序时性能表现一般。
那总结起来呢,在去完成一些简单的程序时,可以使用面向过程去解决。但是如果有复杂的程序或任务,而且需要不断的进行迭代和维护,那么肯定是优先选择面向对象的编程思想
如何学习面向对象编程
那我们后面如何去学习面向对象编程呢?其实就两步:
- 学习面向对象编程的思想
- 学习面向对象编程的语法
这两步中,其实难的是第一步,学习面向对象编程的思想。
不管如何,什么事情都需要有个开头,那我们就从类和对象的基本概念开始好了。
认识类与对象
类: 类是对象的一个抽象的概念
对象(实例):对象就是由类创建的实例
那么这两者的关系其实就是「模具和铸件」之间的关系。
- 类是由对象总结而来的,总结的这个过程叫做抽象。
- 对象是由类具体实施出来的,这个过程叫做实例化。
是不是听着有些迷糊了?这里我们还是用实际例子来解释一下的好,我们思考下面的问题:
- 水果是一个对象还是一个类?
- 汽车是一个对象还是一个类?
- 手机是一个对象还是一个类?
我们在说水果的时候,你能想到什么?香蕉、苹果、西瓜、榴莲等等。对吧?那我们想了这么多不一样的东西,是不是这些所有的都称为是「水果」?那么我们将这些内容都叫做水果的过程就称为「归类」的过程。这个「水果」就是一个类,刚才我们总结的这个过程就叫做抽象,我们想到的香蕉、苹果...等等,就是对象。
汽车其实是一个概念,你能想到什么?奔驰、野马、奥迪、别摸我?那我们见过的车,就会在我们脑海中浮现,而这些具体的车总结出来一个类的过程就是「抽象的过程」,我们最后总结出来的「汽车」就是一个类。那些在我们脑海里浮现的具体的汽车,就是对象。
单我们去开车上班的时候,那么我们就是应用一个具体的对象去发生特定的功能。
再来想一个问题,我现在给大家写这个教程,用的是 Macbook Pro,那么请问我当前正在使用的这个 MBP 是对象还是一个类?
MBP 的特征:金属外壳,优美的外观。
MBP 的功能:给大家写教程,编辑代码,听音乐,作曲,画画....
当我描述了这么多之后,这个 MBP 到底是一个类还是一个对象?
面向对象的基本实现
如果我们需要实例一个对象,那么我们就需要先抽象一个类。
举了栗子:
我们现在需要创建一个汽车,或者千千万万个汽车用于销售。那在这之前我们要做什么?
首先,我们需要抽象一个汽车类,也就是我们要在一个设计图纸上设计处这个汽车。
然后,我们由这个设计图纸去创建(实例)出来的真实汽车就是一个对象。
那么接下来,就让我们具体到代码里去实现看看。
还记得我们之前介绍的怎么去创建一个类嘛?有没有小伙伴还记得?
1 |
|
没错,就是使用class
关键字来定义一个类。其书写规范如下:
1 |
|
那么我们在类里需要声明些什么内容呢?一个类需要有「特征」和「功能」两个内容组成:
特征就是一个描述:颜色:黑色, 品牌:野马,排量:2.4...; 特征在编程中就是一个变量,在类中称为属性
功能就是某一项能力: 拉货,代步,上班....; 功能在编程中就是一个函数,在类中称为方法
在类中,属性一般定义在前面,方法定义在后面。
1 |
|
现在,我们拥有了一个具体的类,里面包含了特征和功能。那么我们如何通过类实例化对象并最终使用它们呢?
很简单,将其赋值给一个具体的变量就可以了,比如,我们现在去 4S 店实际购买一个野马汽车:
1 |
|
这样,就简单的实例化了一个购买的新车,让我们查看一下它的类别和各项属性:
1 |
|
这样,我们就能看到这个对象是由类Car
实例化得来的。并且查看到了品牌属性,试用了一下其“代步”这个功能。
成员属性和方法的操作
一个对象通过实例化之后,在类中定义的属性和方法,可以使用实例化的对象进行操作。
类中定义的属性也称为成员属性,类中定义的方法,也称为成员方法。
我们直接拿之前定义的类来实例化两个对象观察一下:
1 |
|
我们来看,a,b
分别实例化之后,我们将其打印出来。看到两个对象都是通过Car
来实例化的,但是后面不同。就是说,这两者在实例化之后,完全就是两个不同的对象。那我们可以这么说,一个类可以实例化处多个对象。
对象的成员操作
在类的外部,使用对象操作成员,比如,我们可以通过对象访问类中的属性:
1 |
|
还可以通过对象访问类中的方法:
1 |
|
那除了访问,我们是否可以对其进行修改呢?来看看:
1 |
|
可以看到,我们修改了对象的属性。那么,这个时候我们另外一个实例化对象b
里是什么情况?
1 |
|
依然还是black
,并未收到a
内属性变化的影响。
也就是说,我们操作单个对象进行属性修改,并不影响最初的类,也不会影响同一个类实例化出来的其他对象。
我们还可以给对象添加本来没有的属性来丰富这个对象:
1 |
|
同样的,我们对单个对象进行的操作,一样不会影响原本的类以及其他实例化对象:
1 |
|
不出所料的报错了,错误类型为属性错误。告知我们并没有name
这个属性。
好,再让我们来看看删除这个动作。我们就直接删除a
对象刚创建的name
:
1 |
|
程序显示打印了一次name
的值,说明我们能正常获取,然后删除a
对象中的这个属性,然后再打印来看,警告我们AttributeError
类型错误。说明,这个时候的name
已经不存在了。
好,让我们删除a
继承下来的属性brand
,不过这次为了让后续程序还能正常运行,我们使用try
来捕获一下错误。
1 |
|
可以看到,我们在执行删除a.brand
的时候报错了,后面打印的结果也证明了a.brand
这个属性还存在,可以被打印出来。
那么问题来了,为什么之前的a.name
可以被删除,而a.brand
不行?这两个属性到底有什么区别?
其实,单我们执行删除一个对象的属性时,只能删除当前这个对象自己的属性才可以。而我们执行的操作中,brand
并不是a
自己的属性,而是属于Car
这个类的。因为无法进行删除。
a.name
则不一样,是单独在a
对象内创建的属性,因此可以删除。
访问成员属性,会先访问对象自己的属性,如果没有,则去访问这个对象的类的属性。
修改对象的属性值时,实际上等于给这个对象创建了一个对象自己的属性。
添加对象的属性,是给对象创建了自己独有的属性。
删除属性,只能删除这个对象自己的属性,包括给对象添加的和修改的。
接着,我们来看看在类的外部,操作对象的方法。
访问对象的方法:实际上如果这个对象没有自己独立的方法,那么会访问这个对象的类的方法。
1 |
|
我们来进行修改对象的方法:给这个对象的方法重新定义:
1 |
|
这样,我们就完成了方法的重新定义。
访问、修改之后,我们能不能给对象添加新的方法呢?
1 |
|
看来也是可以的,我们现在给这个对象自己新创建了一个方法。
来,删除一下方法试试:
1 |
|
并未报错,我们继续执行下试试:
1 |
|
看报错,说明我们删除成功了。
方法实际上和属性一样,我们可以删除对象自己的方法,但是无法删除对象的类的方法。
至此,我们可以总结如下:
一个类定义类成员属性和成员方法,那么通过这个类实例化的对象,也具备了这些方法和属性。
实际上,创建对象的时候,并不会把类中的属性的属性和方法复制一份给对象,而是在对象中应用父类的方法。因此在访问对象的属性时,会先去找对象自己的属性,如果没有就去找这个类的属性和方法。
一个对象由类创建以后,是一个独立的对象,会应用父类中的属性和方法。如果在对象创建后,给对象的属性或方法,进行修改或添加,那么此时等于给这个对象创建了一个自己的属性和方法。所以在删除时,只能删除对象呗修改或添加的成员。
除了在类的实例化对象中对类的成员进行操作之外,我们还可以直接在类上进行操作。比如,我们可以执行下列操作:
1 |
|
那现在提一个问题,在原始类的成员修改之后,这个类创建的实例化对象会如何?
1 |
|
很明显,我们直接在类上进行操作修改成员之后,不管是 hi 新创建的实例化对象,还是早已存在的实例化对象,其中的成员属性都被修改了。删除和新加都遵循着这样一个特性。
对成员属性和方法的操作,我们也就可以总结成两种,一是「对象操作成员」, 一种是「类操作成员」。当然,由于类修改后会影响具体的实例化对象,所以并不推荐这么去做。
对象操作成员
1 |
|
类操作成员(不推荐)
1 |
|
最终总结一下如下:
- 一个类可以实例化出多个对象,每个对象在内存中都独立存在的
- 当通过类实例化对象时,并不会把类中的成员复制一份给对象,而去给对象了一个引用
- 访问对象成员的时候,如果对象自己没有这个成员,对象会向实例化它的类去查找
- 对象成员的添加和修改,都只会影响当前对象自己,不会影响类和其它对象
- 删除对象的成员时,必须是该对象自己具备的成员才可以,不能删除类中引用的成员
- 对类的成员操作,会影响通过这个类创建的对象,包括之前创建的。
成员方法中的self
self
在方法中只是一个形参,并不是关键字。从它本身的意义上来说,是可以用其他的关键字去替换的,但是长久以来的惯例大家都一直在使用self
。
其作为英文单词的本意是:自己。那么在类的方法中则代表的是「当前这个对象」。不太明白?让我们来看一个实际的例子:
让我们先定义一个「Person」类,然后实例化一个「张三」:
1 |
|
成功打印出了name
, 说明我们成功实例化了。
通过实例化的对象,我们可以在类的外部去访问成员属性和成员方法。(对象.成员)。
同样的,我们其实也可以在类的内部去访问成员属性和成员方法。让我们做一个实验,来说明一下self
到底是什么:
1 |
|
我们修改了这个类,在内部创建了一个方法func(self)
,
然后打印了self
这个参数。
然后我们在外面打印了实例化的zs
,还通过这个具体的实例化对象执行了类内部的func
方法。实际上就是打印了一下此刻的self
。可以看到,两个打印结果完全一样,那说明,这两者本身就是一个东西。
self
代表调用这个方法的对象,谁调用了这个方法,self
就代表的是谁。self
就可以在类的内部代替对象进行各种操作。
我们通过self
来进行的操作,其实完全就是实例化的对象所作的操作。我们在类中修改func
这个方法,让其打印name
,
修改name
, 调用方法rap
来试试看:
1 |
|
我们就可以很清晰的看到self
代表的含义,谁调用,self
就代表谁。也就是说,只要是对象能干的事情,self
就可以代表对象去完成,比如成员的添加、删除、更新、访问、调用等等。
我们再来修改一下类里的方法,让其更清晰的显示这个特性:
1 |
|
在类中,我们修改了一下rap
方法,让其调用self.name
,
在类被定义的时候,这个类中的name
是被赋值为name
的。然后,我们在func
方法中调用了一下self.rap()
,
我们对其进行实例化一个对象zs
,并且在这个实例中对name
进行了重新赋值张三
,
接着,调用了实例化对象中的func()
方法。
我们清晰的看到,func()
调用了self.rap()
,然后将张三
打印在了屏幕上。充分说明了,这个时候的self
代表的就是调用它的zs
这个实例化对象。
我们直接调用类中的方法试试看:
1 |
|
我们收到了报错,被告知缺少必须的位置参数self
。
好,那让我们再来做两个实验,第一个实验中,我们测试一下如果在类中的方法没有使用self
接受参数会怎样:
1 |
|
可以看到,我们可以使用类直接调用这个方法有效,但是我们创建一个实例化对象之后,利用实例化对象去调用则会报错。这个是因为,我们在用实例化对象去调用类中的方法的时候会传入一个参数。但是现在类中的func()
方法并没有可以接受的参数,那么必定会报错。
第二个实验,我们试试不用self
,而是其他的参数是否可以成功:
1 |
|
可以看到,完全没有问题。也就是说,用实例化对象调用类中的方法时,是一定会将自己作为一个参数传给这个方法,需要一个具体的参数去接受。而参数的名称是什么则无所谓,只是大家在习惯上都是用self
。区别如下:
- 含有
self
或者可以接受对象作为参数的方法: 非绑定类方法 - 不含
self
或者不能接受对象作为参数的方法:绑定类方法
非绑定类方法,可以使用对象去访问, 绑定类方法,只能通过类去访问。
魔术方法
魔术方法是什么呢?
魔术方法也和普通方法一样都是类中定义的成员方法。这是一种不需要去手动调用的,在某种情况下,自动触发(自动执行)的方法。魔术方法特殊就特殊在定义的时候,多数的魔术方法 前后都有两个连续的下划线。但是切记,这个方法并不是我们自己定义的,而是系统定义好的,我们来使用而已。
__init__
初始化方法
这个初始化方法是在通过类实例化对象之后,自动触发的一个方法。
1 |
|
注意到了么?我们仅仅是实例化的对象而已,并没有进行任何调用,初始化方法就执行了一遍。那么,我们可以得到下面这些内容:
__init__
触发机制: 在通过类实例化对象后,自动触发的一个方法- 作用:可以在对象实例化之后完成对象的初始化(属性的复制,方法的调用)。
- 应用场景:文件的打开,数据的获取。 干活之前,做好一些准备工作。
以下,我们改造一下这个类,然后再实例化的时候多做一些动作:
1 |
|
当然,我们还可以再初始化方法中调用say
方法,完成自我介绍:
1 |
|
__del__
: 析构方法
和初始化方法一样,我们直接来解析一下这个方法的触发机制,作用以及注意点。
- 触发机制: 析构方法方法会在对象被销毁时自动触发。
- 作用:关闭一些开发的资源
- 注意:对象被销毁时触发了析构方法,而不是析构方法销毁了对象。
我们还是从代码里来观察这个方法。
我们来定义一个类,完成一个日志的记录,调用这个对象的时候,传递一个日志信息。这个对象会创建一个文件,开始写入,并在最后关闭这个文件。
1 |
|
这段代码中,我们实例化了writeLog()
类,调用了初始化方法。在方法中我们打开了文件,因为我用的是变量创建,所以不一定是什么文件。当前我操作的文件为2023-08-16.log
。
然后我们调用l.log()
,
也就是实例化对象中的log
方法来对该文件写入一段日志内容:today is good day.
,
在执行之后,我们又使用了del l
来销毁这个实例。在销毁实例的时候,就会调用__del__
方法来执行其中的方法。
那么对象会在什么情况下被销毁呢?
- 当程序执行完毕,内存中所有的资源都会被销毁释放
- 使用 del 删除时
- 对象没有被引用时,会自动销毁
面向对象的三大特性
面向对象有三大特性,分别是「封装、继承、多态」, 那么它们具体都是什么呢?下面让我们分别来解释。
封装
封装,就是使用特殊的语法,对成员属性和成员方法进行包装,达到保护和隐藏的目的。就像我们送礼的时候,会找东西把礼物包起来一样。
但是一定注意,不能把成员全部封装死,就失去意义了。就好比我们买的笔记本电脑,无论如何都会给你留下一些接口的,比如说电源接口,USB 接口等等。只有有了这些接口,我们才能插上鼠标啊,移动硬盘等等来进行使用。
被封装的成员主要是供类的内部使用。被特殊语法封装的成员,会有不同的访问的权限。比如笔记本内的硬盘,内存等等,这些并不是不让你使用,而是提供给笔记本本身使用,我们可以操作笔记本电脑来达到间接使用它们的目的。
封装分为了几个不同的级别,一般情况下有三种:
公有的 public
受保护的 protected
私有的 private
被特殊语法封装的成员,会有不同的访问权限。
1 |
|
在整段代码中,我们实例化对象的时候,基本可以访问Person
类中所有的成员。我们说定义的属性和方法,都可以无障碍访问。那么,我们现在说定义的这些成员,就都是Public
级别。
现在想象一个场景,我们走在美国街头上,遇到一个美女,然后我们上前询问人家的年龄,大多数时候我们得不到想要的答案。而如果我们上去询问性别(现在知道为什么我要设定为美国街头了吧?),我估计这个就是保密的了吧,有可能一种情况就是当事人在当时的情况下,自己都不知道自己是什么性别。
那这个时候,我们就需要改写一下这段代码了,
改写之前,我们需要理解一下Python
中不同级别成员的定义方式,分别为:
str
=> 公共的_str
=> 受保护的(约定俗成,在 Python 中没有具体实现)__str
=> 私有的。
在了解了定义方法之后,我们可以着手来做实验了:
1 |
|
可以看到,我们调用实例化方法得到的结果已经和之前有所不同了。最终拿到的__sex
成员属性是属于类的。
现在让我们逐一来调用一下:
1 |
|
可以看到,_age
作为受保护的成员属性可以调用,但是__sex
作为私有成员属性则不允许。
实际上,受保护的成员属性也是不能调用的,但是 Python 中因为没有具体实现,所以唯独在 Python 中可以调用。
1 |
|
那么,作为受保护的成员方法_sing
被正常调用了,但是室友的成员方法__kiss
调用的时候报错。看来和成员属性是一致的。
那么我们现在就可以总结如下:
公有的(Public) | 受保护的(Protected) | 私有的(Private) | |
---|---|---|---|
在类的内部 | 可以访问 | 可以访问 | 可以访问 |
在类的外部 | 可以访问 | 不可以访问(Python 中可以) | 不可以访问 |
在实现上我们总结如下:
公有的(Public) | 受保护的(Protected) | 私有的(Private) | |
---|---|---|---|
定义 | 默认定义的成员都属于公有成员 | 在成员名称前面加一个下划线 _成员名称 |
在成员名称前面加两个下划线 __成员名称 |
特征 | 公有的成员可以在任何位置进行访问和操作 | 受保护的成员和公有成员一样可以在任何位置进行访问,但是一般不要随便访问和操作受保护成员 | 私有的成员只能在当前类的内部去访问和操作,不能在类的外部进行操作 |
⚠️ 这里我们需要注意 Python 特殊的亮点:
- 在 python 中并没有实现受保护的封装,属于开发者的约定俗成。
- python 中的私有化封装是通过改名策略实现的,并不是真正的私有化
继承
继承是什么?我们是不是经常听到「文化的继承,技艺的继承,衣钵的继承...」等等这些。
那计算机的继承又是什么?
在面向对象中,一个类去继承父类,那么这个类就拥有了父类中除了私有成员之外的所有成员,包括属性和方法。这个,就叫做继承。
在整个继承过程中,被其他类继承的类就称为「父类」, 也可以称为「基类」或者「超类」。那么继承其他类的类,就被称为「子类」, 也可以称为「派生类」。
那么我们继承又什么意义吗?继承的主要意义,就是为了提高代码的重用性,建立新的类与类的关系,方便其他逻辑的操作。
继承实现起来其实非常方便:
1 |
|
我们直接看代码来理解,比如,我有如下定义:
1 |
|
我们看,猫是不是也是属于猫科动物的一种动物?那么在猫科动物中定义的所有成员,其实在猫这边我也会有。不过这样重复定义是不是感觉特别繁琐?其实,我们在Cat
中完全不需要再次输入这么多,完全可以这样写:
1 |
|
这样,我在定义Cat
的时候就完成了对Felidae
的继承,然后我们实例化一个Cat
,再调用这个实例化对象中的方法run()
,
也就输出了原本是属于类Felidae
中的run()
方法。
我们再继承父类的时候,之类还可以写入自己独有的成员属性或方法。
1 |
|
我们定义Cat
的时候,除了继承Felidae
里的成员之外,还定义了一个size
成员属性和一个eat
成员方法。然后我们在实例化对象中进行调用,都正常运行。
这个时候我们反过来,使用父类Felidae
来调用在之类Cat
中定义的成员,则会报错。说明这个成员是独属于之类的。
我们不仅可以继承的时候进行扩展,还可以复写父类中的方法,使的它与父类方法产生差异化。其方法是在子类中将父类的方法重新定义一遍就可以了。
那有什么办法在我重写父类方法的时候,仍然可以调用父类方法吗?也是可以的,就是使用super().父类方法名()
来进行操作:
1 |
|
我们可以看到,在子类中我们重写了父类中的run
方法,但是由于我们在重写的时候在内部使用了super().run()
。
所以父类中的方法被完全调用了一遍。
所以,我们目前可以总结继承的特征如下:
- 在不指定继承的父类时,所有类都继承自 object 类(系统提供) 了解
- 子类继承了父类后,就拥有了父类中的所有成员包括魔术方法(除了私有成员)
- 子类继承父类后,并不会把父类的成员复制给子类,而去引用
- 子类继承父类后可以重写父类中的方法,叫做 重写
- 子类重写父类的方法,依然可以使用
super().父类方法名()
的方式调用父类的方法 - 子类中如果定义了父类中不存在的方法,称为对父类的扩展
- 一个父类可以被多个子类继承,还可以存在 链式继承 。
- 链式继承:A 类继承了 B 类,B 类继承了 C 类,C 类继承了 D 类。。。
单继承和多继承
一个类只能继承一个父类的方式,就叫做单继承。如果一个类继承了多个父类的方式,就称为多继承。直接看例子,
1 |
|
像代码中定义的Japanese
类,同时继承了Person
和Chusheng
,
那这个,就属于多继承。我们来区分一下语法特征:
1 |
|
在多继承的关系里,有一个有意思的部分,我们来看看:
1 |
|
我们现在看到这段代码是一个多继承关系,我在C
这个类中继承了Tiger
和Cat
两个类,并且复写了eat()
这个方法。按道理来说,我们实例化C
类之后,打印的结果一定是复写的结果。但是我们在C
类的eat
方法里还调用了super().eat()
,
我们知道super()
是调用一遍父类的方法。那么这里到底是调用Tiger
里的eat
方法,还是Cat
里的eat
方法呢?
让我们看打印结果:
1 |
|
打印结果有没有出乎你的意料?那么这个原因是什么呢?其实也不复杂,就是因为Tiger
的调用在前面,Cat
在后面。让我们重新改一下看看:
1 |
|
这就证实了,谁在前面就调用谁的方法。
菱形继承(钻石继承)
先来看一个图形:
1 |
|
那我们先有一个A
类,下面有B
和C
类,再下面还有一个D
类。
看图可能还是不太明白,它们之间的关系是这样的:B
和C
继承了A
类,然后D
又多继承了B
和C
。
那么这种继承关系就叫做菱形继承。
那么我们现在面临的一个问题就是:在这种菱形继承关系中,类与类是什么关系?super()
调用时的顺序是怎样的?
1 |
|
那么我们来看一下,究竟是怎样的一个顺序:
1 |
|
上边这一段中,=>
是继承关系,->
是执行顺序。
好,那我们这个时候要清楚一个点是,我们使用的d
这个实例化去执行的,那么在这所有的继承类中,self
全部都是c
这个实例化对象。让我们来看看到底是不是:
1 |
|
打印的结果证实了我们刚才的说法。
这个地方可能比较让人意外的是之前那个继承关系上,明明我B
继承的是A
,
怎么变成C
了?我们来看看原因:
1 |
|
super
在调用时,并不是查找父类,而是去 MRO
列表上找上一个类。
super
方法在调用时,会自动把当前self
传入到上一级的类的方法中。
所以我们之前会呈现出D=>B=>C=>A
的顺序。
看着有点晕是吧?别着急,我们接下来介绍一个方法,能很方便的看到类关系。
issubclass()
类关系检测
这个方法是检测一个类是否是另一个类的之类的方法。用起来也非常简单:
1 |
|
多态
对于同一个方法,由于调用的对象不同,产生了不同形态的结果。这个就叫做多态。
比如说,我们现在的电脑上有一个 USB 接口,那么这个接口在接入不同的设备的时候,产生的结果也是不一样的。插入鼠标,我们可以点击。插入键盘我们可以输入,插入 U 盘呢,我们可以读取。对吧?对于这个 USB 接口来说。就属于多态。
好的,让我们来实现一下,直接看代码:
1 |
|
这样,我们就实现了一个多态的程序。
我们在实例化Computer()
之后,利用实例化对象c
调用类中的方法usb
,
将实例化对象传入,并且还传入了不同的obj
,
这里的obj
是我们之前实例化过的m, k, u
。
那这样,我们obj
代表了不同的实例化对象,那也就会启动不同的类方法。
那这样呢,属于一个普通的方式来实现,其实对于这段程序,我们还可以使用继承关系来完成。
我们先定义一个接口规范类,其他类都继承这个类,并实现(重写)父类中的方法。由于每个对象实现父类的方式或者过程都不相同,最后的结果是不一样的形态。
1 |
|
我们回来看这段代码,实际上,如果抛开USB
类,我们单独去写后面的类,并且把继承关系去掉。最后是不是也可以进行打印?可以...
可是这样的话,那这三个方法中的satrt
方法之间就毫无关系,继承了USB
中的start
方法,也就是继承了规范。
而且这个继承的形式,和我们之前实现的普通版本其实并无什么差别,虽然代码实现上有不同,可是逻辑上是完全相同的。
好了,关于面向对象,我们就先介绍到这里。不过别着急,并不是讲完了,我们下节课还要接着讲「面向对象」。讲解一些高级语法和思想。小伙伴们记得关注。
另外,面向对象这个东西,确实蛮难的,并不是看我这一两节课就能学懂的。虽然我尽力,但是我还是有自知之明。
在这里给大家推荐一本好书,有它在,你想不懂都难。 ^_^