21. 面向对象及特性

Hi,大家好。我是茶桁。

今天开始,我们要迈向 Python 的另外一个台阶了,那就是面向对象。

面向对象编程(Object Oriented Programming),简称为 OOP,是一种以对象为中心的程序设计思想。

与之相对的,就是面向过程编程(Procedure Oriented Programming), 简称为 POP, 是一种以过程为中心的程序设计思想。

面向对象和面向过程

接下来,让我们先了解一下这两个编程思想到底有什么不同。还记得咱们之前讲过宋丹丹老师小品里的经典的「把大象装进冰箱分几步」吗?小品给出的答案是三步对吧?

  1. 第一步:打开冰箱门
  2. 第二步:把大象装进去
  3. 第三步:关上冰箱门

设计思想的不同

那么这个答案,就是一种面向过程的思维,遇到问题之后,分析解决问题的步骤,然后一步步的去实现。

那么如果是面向对象的话,又该如何去做?

面向对象是通过分析问题中需要的抽象模型,然后根据需要的功能分别去创建模型对象,最终由模型对象来完成程序。那这个「把大象装进冰箱分几步」的问题我们该如何去考虑呢?

首先,面向对象要解决这个问题,需要先建立出抽象模型,比如:

  • 打开冰箱门和关闭冰箱门,这都属于一个冰箱的功能,
  • 大象走进去,这就是大象的功能。
  • 到此时我们就出现了两个抽象模型,一个是冰箱,一个是大象

冰箱具有 打开和关闭的功能,大象具有走路的能力。

分析到这里,就是面向对象的思想,具体完成的话,就是去创建冰箱和大象这两个对象,最终完成这个程序

冰箱对象-开门,大象对象-走进冰箱,冰箱对象-关门

这个问题解决了,我们再来思考一个新的问题:「想吃清蒸鱼怎么办?」

当然是按照做菜的顺序一步一步来对吧?这就是典型的面向过程思维:

  1. 买鱼,买料
  2. 杀鱼和清理,并且腌制
  3. 锅里烧水
  4. 把鱼放进去,开始蒸鱼。
  5. 十分钟后开盖,把鱼端出来,然后浇汁。

这样,一步一步的完成这个愿望,就是面向过程所作的事情。

轮到面向对象,又该如何呢?

  • 需要一个对象:大厨。
  • 告诉大厨,我想吃清蒸鱼。

那么大厨呢,有可能是我们自己训练的,也有可能是其他五星酒店挖过来的。不管如何,这是一个已经完善建立好的对象,我们直接拿来用就可以了。面向对象呢,就是这样寻找具体的对象去解决问题。对于我们来说,调用了对象,而对象完成了这个过程。

当然,具体大厨这个对象里肯定还是一步一步的去完成过程,也就是说,最终面向对象中是由面向过程的体现的。但是思维方式,也就是设计思想是完全不同的。

优缺点

既然有不同之处,那必然是有优缺点的。因为有对比嘛。面向过程有其优点,当然,面向对象也有其缺点。

面向过程的核心是过程,过程就是指几觉问题的步骤。其优缺点非常明显:

  • 优点: 将负责的问题流程化,进而实现简答。
  • 缺点: 扩展性差(更新、维护、迭代)

而面向对象的核心是对象,是一个特征和功能的综合体,其优缺点如下:

  • 优点:可扩展性高
  • 缺点:编程复杂度相对面向过程高一些,这里的复杂度指的是计算机在执行面向对象的程序时性能表现一般。

那总结起来呢,在去完成一些简单的程序时,可以使用面向过程去解决。但是如果有复杂的程序或任务,而且需要不断的进行迭代和维护,那么肯定是优先选择面向对象的编程思想

如何学习面向对象编程

那我们后面如何去学习面向对象编程呢?其实就两步:

  1. 学习面向对象编程的思想
  2. 学习面向对象编程的语法

这两步中,其实难的是第一步,学习面向对象编程的思想。

不管如何,什么事情都需要有个开头,那我们就从类和对象的基本概念开始好了。

认识类与对象

类: 类是对象的一个抽象的概念

对象(实例):对象就是由类创建的实例

那么这两者的关系其实就是「模具和铸件」之间的关系。

  1. 类是由对象总结而来的,总结的这个过程叫做抽象。
  2. 对象是由类具体实施出来的,这个过程叫做实例化。

是不是听着有些迷糊了?这里我们还是用实际例子来解释一下的好,我们思考下面的问题:

  • 水果是一个对象还是一个类?
  • 汽车是一个对象还是一个类?
  • 手机是一个对象还是一个类?

我们在说水果的时候,你能想到什么?香蕉、苹果、西瓜、榴莲等等。对吧?那我们想了这么多不一样的东西,是不是这些所有的都称为是「水果」?那么我们将这些内容都叫做水果的过程就称为「归类」的过程。这个「水果」就是一个类,刚才我们总结的这个过程就叫做抽象,我们想到的香蕉、苹果...等等,就是对象。

汽车其实是一个概念,你能想到什么?奔驰、野马、奥迪、别摸我?那我们见过的车,就会在我们脑海中浮现,而这些具体的车总结出来一个类的过程就是「抽象的过程」,我们最后总结出来的「汽车」就是一个类。那些在我们脑海里浮现的具体的汽车,就是对象。

单我们去开车上班的时候,那么我们就是应用一个具体的对象去发生特定的功能。

再来想一个问题,我现在给大家写这个教程,用的是 Macbook Pro,那么请问我当前正在使用的这个 MBP 是对象还是一个类?

MBP 的特征:金属外壳,优美的外观。

MBP 的功能:给大家写教程,编辑代码,听音乐,作曲,画画....

当我描述了这么多之后,这个 MBP 到底是一个类还是一个对象?

面向对象的基本实现

如果我们需要实例一个对象,那么我们就需要先抽象一个类。

举了栗子:

我们现在需要创建一个汽车,或者千千万万个汽车用于销售。那在这之前我们要做什么?

首先,我们需要抽象一个汽车类,也就是我们要在一个设计图纸上设计处这个汽车。

然后,我们由这个设计图纸去创建(实例)出来的真实汽车就是一个对象。

那么接下来,就让我们具体到代码里去实现看看。

还记得我们之前介绍的怎么去创建一个类嘛?有没有小伙伴还记得?

1
2
3
# 定义一个汽车的类
class Cart():
pass

没错,就是使用class关键字来定义一个类。其书写规范如下:

1
2
3
4
5
'''
类名的书写规范,建议使用驼峰命名法
大驼峰:MyCar ChaHeng
小驼峰:myCar chaHeng
'''

那么我们在类里需要声明些什么内容呢?一个类需要有「特征」和「功能」两个内容组成:

  • 特征就是一个描述:颜色:黑色, 品牌:野马,排量:2.4...; 特征在编程中就是一个变量,在类中称为属性

  • 功能就是某一项能力: 拉货,代步,上班....; 功能在编程中就是一个函数,在类中称为方法

在类中,属性一般定义在前面,方法定义在后面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 定义一个汽车的类
class Car():
# 属性 => 特征 => 变量
color = 'black' # 表示颜色属性
brand = 'mustang' #表示品牌属性
displacement = 2.4 # 表示排量属性

# 方法 => 功能 => 函数
def pulling(self):
print('小汽车能拉货。')

def rode(self):
print('小汽车能代步。')

def onDuty(self):
print('小汽车能上班。')

现在,我们拥有了一个具体的类,里面包含了特征和功能。那么我们如何通过类实例化对象并最终使用它们呢?

很简单,将其赋值给一个具体的变量就可以了,比如,我们现在去 4S 店实际购买一个野马汽车:

1
2
# 实例化一个对象
buyNewCar = Car()

这样,就简单的实例化了一个购买的新车,让我们查看一下它的类别和各项属性:

1
2
3
4
5
6
7
8
9
10
11
12
print(buyNewCar, type(buyNewCar))

# 查看对象的品牌
print(buyNewCar.brand)

# 调用对象的方法
buyNewCar.rode()

---
<__main__.Car object at 0x105e6fbb0> <class '__main__.Car'>
mustang
小汽车能代步。。

这样,我们就能看到这个对象是由类Car实例化得来的。并且查看到了品牌属性,试用了一下其“代步”这个功能。

成员属性和方法的操作

一个对象通过实例化之后,在类中定义的属性和方法,可以使用实例化的对象进行操作。

类中定义的属性也称为成员属性,类中定义的方法,也称为成员方法。

我们直接拿之前定义的类来实例化两个对象观察一下:

1
2
3
4
5
6
7
8
a = Car()
b = Car()
print(a)
print(b)

---
<__main__.Car object at 0x105d37190>
<__main__.Car object at 0x105d36fe0>

我们来看,a,b分别实例化之后,我们将其打印出来。看到两个对象都是通过Car来实例化的,但是后面不同。就是说,这两者在实例化之后,完全就是两个不同的对象。那我们可以这么说,一个类可以实例化处多个对象。

对象的成员操作

在类的外部,使用对象操作成员,比如,我们可以通过对象访问类中的属性:

1
2
3
4
5
res = a.color
print(res)

---
black

还可以通过对象访问类中的方法:

1
2
3
4
a.rode()

---
小汽车能代步。

那除了访问,我们是否可以对其进行修改呢?来看看:

1
2
3
4
5
6
a.color = 'red'
res = a.color
print(res)

---
red

可以看到,我们修改了对象的属性。那么,这个时候我们另外一个实例化对象b里是什么情况?

1
2
3
4
print(b.color)

---
black

依然还是black,并未收到a内属性变化的影响。

也就是说,我们操作单个对象进行属性修改,并不影响最初的类,也不会影响同一个类实例化出来的其他对象。

我们还可以给对象添加本来没有的属性来丰富这个对象:

1
2
3
4
5
a.name = 'AE86'
print(a.name)

---
AE86

同样的,我们对单个对象进行的操作,一样不会影响原本的类以及其他实例化对象:

1
2
3
4
print(b.name)

---
AttributeError: 'Car' object has no attribute 'name'

不出所料的报错了,错误类型为属性错误。告知我们并没有name这个属性。

好,再让我们来看看删除这个动作。我们就直接删除a对象刚创建的name:

1
2
3
4
5
6
7
print(a.name)
del a.name
print(a.name)

---
AE86
AttributeError: 'Car' object has no attribute 'name'

程序显示打印了一次name的值,说明我们能正常获取,然后删除a对象中的这个属性,然后再打印来看,警告我们AttributeError类型错误。说明,这个时候的name已经不存在了。

好,让我们删除a继承下来的属性brand,不过这次为了让后续程序还能正常运行,我们使用try来捕获一下错误。

1
2
3
4
5
6
7
8
9
10
try:
del a.brand
except AttributeError as e:
print('AttributeError:', e)

print('a.brand: ', a.brand)

---
AttributeError: brand
a.brand: mustang

可以看到,我们在执行删除a.brand的时候报错了,后面打印的结果也证明了a.brand这个属性还存在,可以被打印出来。

那么问题来了,为什么之前的a.name可以被删除,而a.brand不行?这两个属性到底有什么区别?

其实,单我们执行删除一个对象的属性时,只能删除当前这个对象自己的属性才可以。而我们执行的操作中,brand并不是a自己的属性,而是属于Car这个类的。因为无法进行删除。

a.name则不一样,是单独在a对象内创建的属性,因此可以删除。

访问成员属性,会先访问对象自己的属性,如果没有,则去访问这个对象的类的属性。

修改对象的属性值时,实际上等于给这个对象创建了一个对象自己的属性。

添加对象的属性,是给对象创建了自己独有的属性。

删除属性,只能删除这个对象自己的属性,包括给对象添加的和修改的。

接着,我们来看看在类的外部,操作对象的方法。

访问对象的方法:实际上如果这个对象没有自己独立的方法,那么会访问这个对象的类的方法。

1
2
3
4
a.rode()

---
小汽车能代步

我们来进行修改对象的方法:给这个对象的方法重新定义:

1
2
3
4
5
6
7
8
def func():
print('这里是重新定义的一个方法')

a.rode = func
a.rode()

---
这里是重新定义的一个方法

这样,我们就完成了方法的重新定义。

访问、修改之后,我们能不能给对象添加新的方法呢?

1
2
3
4
5
a.func2 = func
a.func2()

---
这里是重新定义的一个方法

看来也是可以的,我们现在给这个对象自己新创建了一个方法。

来,删除一下方法试试:

1
del a.func2

并未报错,我们继续执行下试试:

1
2
3
4
a.func2()

---
AttributeError: 'Car' object has no attribute 'func2'

看报错,说明我们删除成功了。

方法实际上和属性一样,我们可以删除对象自己的方法,但是无法删除对象的类的方法。

至此,我们可以总结如下:

一个类定义类成员属性和成员方法,那么通过这个类实例化的对象,也具备了这些方法和属性。

实际上,创建对象的时候,并不会把类中的属性的属性和方法复制一份给对象,而是在对象中应用父类的方法。因此在访问对象的属性时,会先去找对象自己的属性,如果没有就去找这个类的属性和方法。

一个对象由类创建以后,是一个独立的对象,会应用父类中的属性和方法。如果在对象创建后,给对象的属性或方法,进行修改或添加,那么此时等于给这个对象创建了一个自己的属性和方法。所以在删除时,只能删除对象呗修改或添加的成员。

除了在类的实例化对象中对类的成员进行操作之外,我们还可以直接在类上进行操作。比如,我们可以执行下列操作:

1
Car.brand = 'BMW'

那现在提一个问题,在原始类的成员修改之后,这个类创建的实例化对象会如何?

1
2
3
4
5
6
7
8
9
10
print(a.brand) # 先执行一次打印,原始属性
Car.brand = 'BMW'
b = Car() # 新创建一个实例化对象
print(b.brand) # 打印新创建的对象的属性
print(a.brand) # 打印修改之前创建的对象的属性

---
mustang
BMW
BMW

很明显,我们直接在类上进行操作修改成员之后,不管是 hi 新创建的实例化对象,还是早已存在的实例化对象,其中的成员属性都被修改了。删除和新加都遵循着这样一个特性。

对成员属性和方法的操作,我们也就可以总结成两种,一是「对象操作成员」, 一种是「类操作成员」。当然,由于类修改后会影响具体的实例化对象,所以并不推荐这么去做。

对象操作成员

1
2
3
4
5
6
7
8
9
10
11
成员属性:
访问: 对象.成员属性名
修改: 对象.成员属性名法 = 新值。(此时等于给这个对象创建了一个自己的属性)
添加: 对象.新成员属性 = 值 (此时是给这个对象自己新建了一个属性)
删除: del 对象.成员属性 (注意:只能删除这个对象自己的属性)

成员方法:
访问: 对象.成员方法名()
修改: 对象.成员方法名 = func(此时等于给这个对象创建了一个自己的方法)
添加: 对象.方法名 = func (此时是给这个对象自己新建了一个方法)
删除: del 对象.方法名 (注意:只能删除这个对象自己的方法)

类操作成员(不推荐)

1
2
3
4
5
6
7
8
9
10
11
成员属性:
访问: 类名.成员属性名
修改: 类名.成员属性名法 = 新值。(此时通过这个类创建的对象都具有这个属性)
添加: 类名.新成员属性 = 值 (此时通过这个类创建的对象都具有这个属性)
删除: del 类名.成员属性 (注意:删除这个类的属性后,这个类创建的对象也没有这几个属性了)

成员方法:
访问: 类名.成员方法名()
修改: 类名.成员方法名 = func(此时通过类创建的对象都被修改)
添加: 类名.方法名 = func (此时通过类创建的对象都被修改)
删除: del 类名.方法名 (注意:此时通过类创建的对象都被修改)

最终总结一下如下:

  • 一个类可以实例化出多个对象,每个对象在内存中都独立存在的
  • 当通过类实例化对象时,并不会把类中的成员复制一份给对象,而去给对象了一个引用
  • 访问对象成员的时候,如果对象自己没有这个成员,对象会向实例化它的类去查找
  • 对象成员的添加和修改,都只会影响当前对象自己,不会影响类和其它对象
  • 删除对象的成员时,必须是该对象自己具备的成员才可以,不能删除类中引用的成员
  • 对类的成员操作,会影响通过这个类创建的对象,包括之前创建的。

成员方法中的self

self在方法中只是一个形参,并不是关键字。从它本身的意义上来说,是可以用其他的关键字去替换的,但是长久以来的惯例大家都一直在使用self

其作为英文单词的本意是:自己。那么在类的方法中则代表的是「当前这个对象」。不太明白?让我们来看一个实际的例子:

让我们先定义一个「Person」类,然后实例化一个「张三」:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 定义人
class Person():
# 成员属性
name = 'name'
age = 0
sex = 'sex'

# 成员方法
def sing(self):
print('会唱歌')

def dance(self):
print('会跳舞')

def rap(self):
print('会饶舌')


# 实例化对象
zs = Person()
print(zs.name)

---
name

成功打印出了name, 说明我们成功实例化了。

通过实例化的对象,我们可以在类的外部去访问成员属性和成员方法。(对象.成员)。

同样的,我们其实也可以在类的内部去访问成员属性和成员方法。让我们做一个实验,来说明一下self到底是什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 定义人
class Person():
# 成员属性
...
# 成员方法
...
def func(self):
print(self)

# 实例化对象
zs = Person()
# print(zs.name)
print(zs)
zs.func()

---
<__main__.Person object at 0x10a048c10>
<__main__.Person object at 0x10a048c10>

我们修改了这个类,在内部创建了一个方法func(self), 然后打印了self这个参数。

然后我们在外面打印了实例化的zs,还通过这个具体的实例化对象执行了类内部的func方法。实际上就是打印了一下此刻的self。可以看到,两个打印结果完全一样,那说明,这两者本身就是一个东西。

self代表调用这个方法的对象,谁调用了这个方法,self就代表的是谁。self就可以在类的内部代替对象进行各种操作。

我们通过self来进行的操作,其实完全就是实例化的对象所作的操作。我们在类中修改func这个方法,让其打印name, 修改name, 调用方法rap来试试看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 定义人
class Person():
# 成员属性
...

# 成员方法
...
def func(self):
print(self)
print(self.name)
self.name = '茶桁'
print(self.name)
self.rap()

# 实例化对象
zs = Person()
zs.name = "张三"
zs.func()

---
<__main__.Person object at 0x10a06bf40>
张三
茶桁
会饶舌

我们就可以很清晰的看到self代表的含义,谁调用,self就代表谁。也就是说,只要是对象能干的事情,self就可以代表对象去完成,比如成员的添加、删除、更新、访问、调用等等。

我们再来修改一下类里的方法,让其更清晰的显示这个特性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 定义人
class Person():
# 成员属性
name = 'name'
...

# 成员方法
...
def rap(self):
print(f'我是{self.name}, 我会饶舌')

def func(self):
...
self.rap()

# 实例化对象
zs = Person()
zs.name = "张三"
zs.func()

---
我是张三, 我会饶舌

在类中,我们修改了一下rap方法,让其调用self.name, 在类被定义的时候,这个类中的name是被赋值为name的。然后,我们在func方法中调用了一下self.rap(), 我们对其进行实例化一个对象zs,并且在这个实例中对name进行了重新赋值张三, 接着,调用了实例化对象中的func()方法。

我们清晰的看到,func()调用了self.rap(),然后将张三打印在了屏幕上。充分说明了,这个时候的self代表的就是调用它的zs这个实例化对象。

我们直接调用类中的方法试试看:

1
2
3
4
Person.func()

---
TypeError: Person.func() missing 1 required positional argument: 'self'

我们收到了报错,被告知缺少必须的位置参数self

好,那让我们再来做两个实验,第一个实验中,我们测试一下如果在类中的方法没有使用self接受参数会怎样:

1
2
3
4
5
6
7
8
9
10
11
12
class Person():
def func():
print('我是一个没有`self`的方法。')

Person.func()

a = Person()
a.func()

---
我是一个没有`self`的方法。
TypeError: Person.func() takes 0 positional arguments but 1 was given

可以看到,我们可以使用类直接调用这个方法有效,但是我们创建一个实例化对象之后,利用实例化对象去调用则会报错。这个是因为,我们在用实例化对象去调用类中的方法的时候会传入一个参数。但是现在类中的func()方法并没有可以接受的参数,那么必定会报错。

第二个实验,我们试试不用self,而是其他的参数是否可以成功:

1
2
3
4
5
6
7
8
9
10
class Person():
def func(vars):
print(f'我是{vars.name}, 我使用了 vars 来接受参数。')

a = Person()
a.name = 'admin'
a.func()

---
我是 admin, 我使用了 vars 来接受参数。

可以看到,完全没有问题。也就是说,用实例化对象调用类中的方法时,是一定会将自己作为一个参数传给这个方法,需要一个具体的参数去接受。而参数的名称是什么则无所谓,只是大家在习惯上都是用self。区别如下:

  • 含有self或者可以接受对象作为参数的方法: 非绑定类方法
  • 不含self或者不能接受对象作为参数的方法:绑定类方法

非绑定类方法,可以使用对象去访问, 绑定类方法,只能通过类去访问。

魔术方法

魔术方法是什么呢?

魔术方法也和普通方法一样都是类中定义的成员方法。这是一种不需要去手动调用的,在某种情况下,自动触发(自动执行)的方法。魔术方法特殊就特殊在定义的时候,多数的魔术方法 前后都有两个连续的下划线。但是切记,这个方法并不是我们自己定义的,而是系统定义好的,我们来使用而已。

__init__ 初始化方法

这个初始化方法是在通过类实例化对象之后,自动触发的一个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person():
name = None
age = None
sex = None

# 初始化方法
def __init__(self):
print('我是一个初始化方法。')

# 成员方法
def say(self):
print('大家好,我是茶桁。')

# 实例化对象
zs = Person()

---
我是一个初始化方法。

注意到了么?我们仅仅是实例化的对象而已,并没有进行任何调用,初始化方法就执行了一遍。那么,我们可以得到下面这些内容:

  • __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
class Person():
name = None
age = None
sex = None

# 初始化方法
def __init__(self,name,age, sex):
print('我是一个初始化方法。')
# 完成对象属性的初始化赋值
self.name = name
self.age = age
self.sex = sex

# 成员方法
def say(self):
print('大家好,我是茶桁。')

# 实例化对象
zs = Person('张三', 41, 'male')
print(f'我叫{zs.name}, 我今年{zs.age}岁,性别:{zs.sex}')

---
我是一个初始化方法。
我叫张三, 我今年 41 岁,性别:male

当然,我们还可以再初始化方法中调用say方法,完成自我介绍:

1
2
3
4
5
6
def __init__(self, name, age, sex):
...
self.say()

def say(self):
print(f'打击好,我是{self.name}。')

__del__: 析构方法

和初始化方法一样,我们直接来解析一下这个方法的触发机制,作用以及注意点。

  • 触发机制: 析构方法方法会在对象被销毁时自动触发。
  • 作用:关闭一些开发的资源
  • 注意:对象被销毁时触发了析构方法,而不是析构方法销毁了对象。

我们还是从代码里来观察这个方法。

我们来定义一个类,完成一个日志的记录,调用这个对象的时候,传递一个日志信息。这个对象会创建一个文件,开始写入,并在最后关闭这个文件。

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
import time
class writeLog():
# 成员属性
# 文件的路径
fileurl = './data'
# 日志文件的名称
filename = str(time.strftime('%Y-%m-%d'))+'.log'
# 初始化 打开文件
def __init__(self):
# 完成文件的打开
print('初始化方法触发类,完成文件的打开')
self.fileobj = open(self.fileurl+self.filename, 'a+', encoding='utf-8')

# 写日志的方法
def log(self,s):
print(f'把日志{s}写入到文件中')

# 析构方法
def __del__(self):
print('析构方法触发了,关闭打开的文件')
# 在对象被销毁时,关闭在初始化方法中打开的文件对象
self.fileobj.close()

l = writeLog()
l.log('today is good day.')
del l

---
初始化方法触发类,完成文件的打开
把日志 today is good day.写入到文件中
析构方法触发了,关闭打开的文件

这段代码中,我们实例化了writeLog()类,调用了初始化方法。在方法中我们打开了文件,因为我用的是变量创建,所以不一定是什么文件。当前我操作的文件为2023-08-16.log

然后我们调用l.log(), 也就是实例化对象中的log方法来对该文件写入一段日志内容:today is good day., 在执行之后,我们又使用了del l来销毁这个实例。在销毁实例的时候,就会调用__del__方法来执行其中的方法。

那么对象会在什么情况下被销毁呢?

  1. 当程序执行完毕,内存中所有的资源都会被销毁释放
  2. 使用 del 删除时
  3. 对象没有被引用时,会自动销毁

面向对象的三大特性

面向对象有三大特性,分别是「封装、继承、多态」, 那么它们具体都是什么呢?下面让我们分别来解释。

封装

封装,就是使用特殊的语法,对成员属性和成员方法进行包装,达到保护和隐藏的目的。就像我们送礼的时候,会找东西把礼物包起来一样。

但是一定注意,不能把成员全部封装死,就失去意义了。就好比我们买的笔记本电脑,无论如何都会给你留下一些接口的,比如说电源接口,USB 接口等等。只有有了这些接口,我们才能插上鼠标啊,移动硬盘等等来进行使用。

被封装的成员主要是供类的内部使用。被特殊语法封装的成员,会有不同的访问的权限。比如笔记本内的硬盘,内存等等,这些并不是不让你使用,而是提供给笔记本本身使用,我们可以操作笔记本电脑来达到间接使用它们的目的。

封装分为了几个不同的级别,一般情况下有三种:

公有的 public

受保护的 protected

私有的 private

被特殊语法封装的成员,会有不同的访问权限。

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
class Person():
# 成员属性
name = None
age = None
sex = None

# 初始化方法
def __init__(self, name, age, sex):
self.name = name
self.age = age
self.sex = sex

# 成员方法
def say(self):
print('talk about life.')

def sing(self):
print('sing a song.')

def kiss(self):
print('come on...')

# 实例化对象
zs = Person('张三', 49, 'male')

# 查看对象的所有成员

print(Person.__dict__) # 获取当前类的所有成员信息
print(zs.__dict__) # 获取当前对象的所有成员信息

# 我们也可以直接访问对象所有的方法
print(zs.name)
zs.kiss()

---
{'__module__': '__main__', 'name': None, 'age': None, 'sex': None, '__init__': <function Person.__init__ at 0x111a355a0>, 'say': <function Person.say at 0x111a35750>, 'sing': <function Person.sing at 0x111a357e0>, 'kiss': <function Person.kiss at 0x111a35870>, '__dict__': <attribute '__dict__' of 'Person' objects>, '__weakref__': <attribute '__weakref__' of 'Person' objects>, '__doc__': None}
{'name': '张三', 'age': 49, 'sex': 'male'}
张三
come on...

在整段代码中,我们实例化对象的时候,基本可以访问Person类中所有的成员。我们说定义的属性和方法,都可以无障碍访问。那么,我们现在说定义的这些成员,就都是Public级别。

现在想象一个场景,我们走在美国街头上,遇到一个美女,然后我们上前询问人家的年龄,大多数时候我们得不到想要的答案。而如果我们上去询问性别(现在知道为什么我要设定为美国街头了吧?),我估计这个就是保密的了吧,有可能一种情况就是当事人在当时的情况下,自己都不知道自己是什么性别。

那这个时候,我们就需要改写一下这段代码了, 改写之前,我们需要理解一下Python中不同级别成员的定义方式,分别为:

  • str => 公共的
  • _str => 受保护的(约定俗成,在 Python 中没有具体实现)
  • __str => 私有的。

在了解了定义方法之后,我们可以着手来做实验了:

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
class Person():
# 成员属性
name = None
_age = None # 这是一个 protected 成员属性
__sex = None # 这是一个 private 成员属性

# 初始化方法
def __init__(self, name, age, sex):
self.name = name
self._age = age
self.__sex = sex

# 成员方法
def say(self):
print('talk about life.')

def _sing(self): # 这是一个 protected 成员方法
print('sing a song.')

def __kiss(self): # 这是一个 private 成员方法
print('come on...')

# 实例化对象
zs = Person('张三', 49, 'male')

# 查看对象的所有成员

print(Person.__dict__) # 获取当前类的所有成员信息
print(zs.__dict__) # 获取当前对象的所有成员信息

---
{'__module__': '__main__', 'name': None, '_age': None, '_Person__sex': None, '__init__': <function Person.__init__ at 0x111f99630>, 'say': <function Person.say at 0x111f996c0>, 'sing': <function Person.sing at 0x111f99870>, 'kiss': <function Person.kiss at 0x111f99c60>, '__dict__': <attribute '__dict__' of 'Person' objects>, '__weakref__': <attribute '__weakref__' of 'Person' objects>, '__doc__': None}
{'name': '张三', '_age': 49, '_Person__sex': 'male'}

可以看到,我们调用实例化方法得到的结果已经和之前有所不同了。最终拿到的__sex成员属性是属于类的。

现在让我们逐一来调用一下:

1
2
3
4
5
6
print(zs._age)
print(zs.__sex)

---
49
AttributeError: 'Person' object has no attribute '__sex'

可以看到,_age作为受保护的成员属性可以调用,但是__sex作为私有成员属性则不允许。

实际上,受保护的成员属性也是不能调用的,但是 Python 中因为没有具体实现,所以唯独在 Python 中可以调用。

1
2
3
4
5
6
zs._sing()
zs.__kiss()

---
sing a song.
AttributeError: 'Person' object has no attribute '__kiss'

那么,作为受保护的成员方法_sing被正常调用了,但是室友的成员方法__kiss调用的时候报错。看来和成员属性是一致的。

那么我们现在就可以总结如下:

公有的(Public) 受保护的(Protected) 私有的(Private)
在类的内部 可以访问 可以访问 可以访问
在类的外部 可以访问 不可以访问(Python 中可以) 不可以访问

在实现上我们总结如下:

公有的(Public) 受保护的(Protected) 私有的(Private)
定义 默认定义的成员都属于公有成员 在成员名称前面加一个下划线 _成员名称 在成员名称前面加两个下划线 __成员名称
特征 公有的成员可以在任何位置进行访问和操作 受保护的成员和公有成员一样可以在任何位置进行访问,但是一般不要随便访问和操作受保护成员 私有的成员只能在当前类的内部去访问和操作,不能在类的外部进行操作

⚠️ 这里我们需要注意 Python 特殊的亮点:

  1. 在 python 中并没有实现受保护的封装,属于开发者的约定俗成。
  2. python 中的私有化封装是通过改名策略实现的,并不是真正的私有化

继承

继承是什么?我们是不是经常听到「文化的继承,技艺的继承,衣钵的继承...」等等这些。

那计算机的继承又是什么?

在面向对象中,一个类去继承父类,那么这个类就拥有了父类中除了私有成员之外的所有成员,包括属性和方法。这个,就叫做继承。

在整个继承过程中,被其他类继承的类就称为「父类」, 也可以称为「基类」或者「超类」。那么继承其他类的类,就被称为「子类」, 也可以称为「派生类」。

那么我们继承又什么意义吗?继承的主要意义,就是为了提高代码的重用性,建立新的类与类的关系,方便其他逻辑的操作。

继承实现起来其实非常方便:

1
2
3
4
5
6
# 继承的语法格式
class 父类():
pass

class 子类(父类):
pass

我们直接看代码来理解,比如,我有如下定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 定义猫科动物
class Felidae():
# 属性
coatColor = 'orange' # 毛色
sex = 'M' # 定义性别

# 成员方法
def run(self):
print('轻盈的跳跃')

def walk(self):
print('走的猫步')

# 定义猫
class Cat():
coatColor = ' white'
sex = 'M' # 定义性别

# 成员方法
def run(self):
print('轻盈的跳跃')

def walk(self):
print('走的猫步')

我们看,猫是不是也是属于猫科动物的一种动物?那么在猫科动物中定义的所有成员,其实在猫这边我也会有。不过这样重复定义是不是感觉特别繁琐?其实,我们在Cat中完全不需要再次输入这么多,完全可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 定义猫科动物
class Felidae():
# 属性
coatColor = 'orange' # 毛色
sex = 'M' # 定义性别

# 成员方法
def run(self):
print('轻盈的跳跃')

def walk(self):
print('走的猫步')

# 定义猫
class Cat(Felidae):
pass

mimi = Cat()
mimi.run()

这样,我在定义Cat的时候就完成了对Felidae的继承,然后我们实例化一个Cat,再调用这个实例化对象中的方法run(), 也就输出了原本是属于类Felidae中的run()方法。

我们再继承父类的时候,之类还可以写入自己独有的成员属性或方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Cat(Felidae):
size = 'small'
def eat(self):
print('吃猫粮。')
pass

mimi = Cat()
mimi.run()
print(mimi.size)
mimi.eat()
Felidae.eat()

---
轻盈的跳跃
small
吃猫粮。

AttributeError: type object 'Felidae' has no attribute 'eat'

我们定义Cat的时候,除了继承Felidae里的成员之外,还定义了一个size成员属性和一个eat成员方法。然后我们在实例化对象中进行调用,都正常运行。

这个时候我们反过来,使用父类Felidae来调用在之类Cat中定义的成员,则会报错。说明这个成员是独属于之类的。

我们不仅可以继承的时候进行扩展,还可以复写父类中的方法,使的它与父类方法产生差异化。其方法是在子类中将父类的方法重新定义一遍就可以了。

那有什么办法在我重写父类方法的时候,仍然可以调用父类方法吗?也是可以的,就是使用super().父类方法名()来进行操作:

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
# 定义猫科动物
class Felidae():
# 属性
coatColor = 'orange' # 毛色
sex = 'M' # 定义性别

# 成员方法
def run(self):
print('轻盈的跳跃')

def walk(self):
print('走的猫步')

# 定义猫
class Cat(Felidae):
size = 'small'
def run(self):
super().run()
print('更加轻盈的跳跃。')

def eat(self):
print('吃猫粮。')

pass

mimi = Cat()
mimi.run()

---
轻盈的跳跃
更加轻盈的跳跃。

我们可以看到,在子类中我们重写了父类中的run方法,但是由于我们在重写的时候在内部使用了super().run()。 所以父类中的方法被完全调用了一遍。

所以,我们目前可以总结继承的特征如下:

  • 在不指定继承的父类时,所有类都继承自 object 类(系统提供) 了解
  • 子类继承了父类后,就拥有了父类中的所有成员包括魔术方法(除了私有成员)
  • 子类继承父类后,并不会把父类的成员复制给子类,而去引用
  • 子类继承父类后可以重写父类中的方法,叫做 重写
  • 子类重写父类的方法,依然可以使用super().父类方法名()的方式调用父类的方法
  • 子类中如果定义了父类中不存在的方法,称为对父类的扩展
  • 一个父类可以被多个子类继承,还可以存在 链式继承 。
    • 链式继承:A 类继承了 B 类,B 类继承了 C 类,C 类继承了 D 类。。。

单继承和多继承

一个类只能继承一个父类的方式,就叫做单继承。如果一个类继承了多个父类的方式,就称为多继承。直接看例子,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person():
print('人的样子。')

class Chusheng():
print('畜生的特性。')

class Japanese(Person, Chusheng):
pass

c = Japanese()
c

---
人的样子。
畜生的特性。

像代码中定义的Japanese类,同时继承了PersonChusheng, 那这个,就属于多继承。我们来区分一下语法特征:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 单继承
class 父类():
pass

class 子类(父类):
pass

# 多继承
class ():
pass

class ():
pass

class (父,母):
pass

在多继承的关系里,有一个有意思的部分,我们来看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Tiger():
def eat(self):
print('大口撕咬食物...')

class Cat():
def eat(self):
print('小口吞咽食物...')

class C(Tiger, Cat):
def eat(self):
super().eat()
print('到底该怎么吃?')

# 实例化对象
c = C()
c.eat()

我们现在看到这段代码是一个多继承关系,我在C这个类中继承了TigerCat两个类,并且复写了eat()这个方法。按道理来说,我们实例化C类之后,打印的结果一定是复写的结果。但是我们在C类的eat方法里还调用了super().eat(), 我们知道super()是调用一遍父类的方法。那么这里到底是调用Tiger里的eat方法,还是Cat里的eat方法呢?

让我们看打印结果:

1
2
3
---
大口撕咬食物...
到底该怎么吃?

打印结果有没有出乎你的意料?那么这个原因是什么呢?其实也不复杂,就是因为Tiger的调用在前面,Cat在后面。让我们重新改一下看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Tiger():
def eat(self):
print('大口撕咬食物...')

class Cat():
def eat(self):
print('小口吞咽食物...')

class C(Cat,Tiger):
def eat(self):
super().eat()
print('到底该怎么吃?')

# 实例化对象
c = C()
c.eat()

---
小口吞咽食物...
到底该怎么吃?

这就证实了,谁在前面就调用谁的方法。

菱形继承(钻石继承)

先来看一个图形:

1
2
3
  A
B C
D

那我们先有一个A类,下面有BC类,再下面还有一个D类。

看图可能还是不太明白,它们之间的关系是这样的:BC继承了A类,然后D又多继承了BC

那么这种继承关系就叫做菱形继承。

那么我们现在面临的一个问题就是:在这种菱形继承关系中,类与类是什么关系?super()调用时的顺序是怎样的?

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
# 菱形继承

# 祖先
class A():
num = 111
def eat(self):
print('学着凭借本能寻找食物...')

# 父亲
class B(A):
num = 222
def eat(self):
super().eat()
print(super().num)
print('进化了,学会大口吃肉。。。')

# 母亲
class C(A):
num = 333
def eat(self):
super().eat()
print(super().num)
print('进化的另外一个分支,小口吞咽...')

# 子
class D(B, C):
num = 444
def eat(self):
super().eat()
print(super().num)
print('居然退化了,又忘了怎么吃...')

d = D()
d.eat()

---
学着凭借本能寻找食物...
111
进化的另外一个分支,小口吞咽...
333
进化了,学会大口吃肉。。。
222
居然退化了,又忘了怎么吃...

那么我们来看一下,究竟是怎样的一个顺序:

1
D.super() => B.super() =>C.super() => A.print() -> C.print() -> B.print() -> D.print()

上边这一段中,=>是继承关系,->是执行顺序。

好,那我们这个时候要清楚一个点是,我们使用的d这个实例化去执行的,那么在这所有的继承类中,self全部都是c这个实例化对象。让我们来看看到底是不是:

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
# 菱形继承

# 祖先
class A():
num = 111
def eat(self):
print(self.num)
print(self)
print('学着凭借本能寻找食物...')

# 父亲
class B(A):
num = 222
def eat(self):
print(self.num)
print(self)
super().eat()
print(super().num)
print('进化了,学会大口吃肉。。。')

# 母亲
class C(A):
num = 333
def eat(self):
print(self.num)
print(self)
super().eat()
print(super().num)
print('进化的另外一个分支,小口吞咽...')

# 子
class D(B, C):
num = 444
def eat(self):
super().eat()
print(super().num)
print('居然退化了,又忘了怎么吃...')

d = D()
d.eat()

---
444
<__main__.D object at 0x111b71540>
444
<__main__.D object at 0x111b71540>
444
<__main__.D object at 0x111b71540>
学着凭借本能寻找食物...
111
进化的另外一个分支,小口吞咽...
333
进化了,学会大口吃肉。。。
222
居然退化了,又忘了怎么吃...

打印的结果证实了我们刚才的说法。

这个地方可能比较让人意外的是之前那个继承关系上,明明我B继承的是A, 怎么变成C了?我们来看看原因:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
'''
在定义类之后,程序会自动生成一个继承的列表 MRO(Method Realtion Order)方法关系列表
MRO 列表生成原则:
1. 子类永远在父类的前面
2. 同一等级的类,按照之类中的继承顺序摆放
3. 先之类,后父类的顺序原则,最终的类是系统提供的 obejct 类

MRO 的调用方法
类名.mro()
'''
D.mro()

---
[__main__.D, __main__.B, __main__.C, __main__.A, object]

super在调用时,并不是查找父类,而是去 MRO 列表上找上一个类。

super方法在调用时,会自动把当前self传入到上一级的类的方法中。

所以我们之前会呈现出D=>B=>C=>A的顺序。

看着有点晕是吧?别着急,我们接下来介绍一个方法,能很方便的看到类关系。

issubclass()类关系检测

这个方法是检测一个类是否是另一个类的之类的方法。用起来也非常简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
res = issubclass(D, B)
print(res)
res = issubclass(D, C)
print(res)
res = issubclass(D, A)
print(res)
res = issubclass(A, D)
print(res)

---
True
True
True
False

多态

对于同一个方法,由于调用的对象不同,产生了不同形态的结果。这个就叫做多态。

比如说,我们现在的电脑上有一个 USB 接口,那么这个接口在接入不同的设备的时候,产生的结果也是不一样的。插入鼠标,我们可以点击。插入键盘我们可以输入,插入 U 盘呢,我们可以读取。对吧?对于这个 USB 接口来说。就属于多态。

好的,让我们来实现一下,直接看代码:

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
# 定义电脑类
class Computer():
# 在电脑类中定义一个 sub 的规范的接口 方法
def usb(self,obj):
obj.start()

# 定义鼠标类
class Mouse():
def start(self):
print('鼠标启动成功,可以双击单击嗨起来。。。')

# 定义键盘类
class KeyBord():
def start(self):
print('键盘启动成功了,赶紧输入 666。。。')

# 定义 U 盘 类
class Udisk():
def start(self):
print('U 盘启动了,赶紧检查一下我的种子还在不在。。。')

# 实例化对象
c = Computer() # 电脑对象
m = Mouse() # 鼠标对象
k = KeyBord() # 键盘对象
u = Udisk() # u 盘对象


# 把不同的设备插入到电脑的 usb 的接口中
c.usb(m)
c.usb(k)
c.usb(u)

---
鼠标启动成功,可以双击单击嗨起来。。。
键盘启动成功了,赶紧输入 666。。。
U 盘启动了,赶紧检查一下我的种子还在不在。。。

这样,我们就实现了一个多态的程序。

我们在实例化Computer()之后,利用实例化对象c调用类中的方法usb, 将实例化对象传入,并且还传入了不同的obj, 这里的obj是我们之前实例化过的m, k, u。 那这样,我们obj代表了不同的实例化对象,那也就会启动不同的类方法。

那这样呢,属于一个普通的方式来实现,其实对于这段程序,我们还可以使用继承关系来完成。

我们先定义一个接口规范类,其他类都继承这个类,并实现(重写)父类中的方法。由于每个对象实现父类的方式或者过程都不相同,最后的结果是不一样的形态。

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
# 继承关系写多态

# 定义 USB
class USB():
'''
info:
这个类是一个接口规范类,需要子类继承并实现 start 方法
start 方法不做任何具体功能的实现
'''
# 在 usb 类中定义一个规范的接口方法,但是不实现任何功能
def start(self):
pass

# 定义鼠标类
class Mouse(USB):
def start(self):
print('鼠标启动成功,可以双击单击嗨起来。。。')

# 定义键盘类
class KeyBord(USB):
def start(self):
print('键盘启动成功了,赶紧输入 666。。。')

# 定义 U 盘 类
class Udisk(USB):
def start(self):
print('U 盘启动了,赶紧检查一下我的种子还在不在。。。')

# 实例化对象
m = Mouse()
k = KeyBord()
u = Udisk()

m.start()
k.start()
u.start()

---
鼠标启动成功,可以双击单击嗨起来。。。
键盘启动成功了,赶紧输入 666。。。
U 盘启动了,赶紧检查一下我的种子还在不在。。。

我们回来看这段代码,实际上,如果抛开USB类,我们单独去写后面的类,并且把继承关系去掉。最后是不是也可以进行打印?可以...

可是这样的话,那这三个方法中的satrt方法之间就毫无关系,继承了USB中的start方法,也就是继承了规范。

而且这个继承的形式,和我们之前实现的普通版本其实并无什么差别,虽然代码实现上有不同,可是逻辑上是完全相同的。

好了,关于面向对象,我们就先介绍到这里。不过别着急,并不是讲完了,我们下节课还要接着讲「面向对象」。讲解一些高级语法和思想。小伙伴们记得关注。

另外,面向对象这个东西,确实蛮难的,并不是看我这一两节课就能学懂的。虽然我尽力,但是我还是有自知之明。

在这里给大家推荐一本好书,有它在,你想不懂都难。 ^_^

领取优惠券购买

img

作者

Hivan Du

发布于

2023-08-16

更新于

2024-01-16

许可协议

评论