21. 面向对象及特性

2023/08/16閱讀時間約 60 分鐘
raw-image


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到底是一个类还是一个对象?

面向对象的基本实现

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

举了栗子:

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

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

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

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

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


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


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


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


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

  • 特征就是一个描述:颜色:黑色, 品牌:野马,排量:2.4...; 特征在编程中就是一个变量,在类中称为属性
  • 功能就是某一项能力: 拉货,代步,上班....; 功能在编程中就是一个函数,在类中称为方法

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


# 定义一个汽车的类
class Car():
# 属性 => 特征 => 变量
color = 'black' # 表示颜色属性
brand = 'mustang' #表示品牌属性
displacement = 2.4 # 表示排量属性

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

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

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


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

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


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


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


print(buyNewCar, type(buyNewCar))

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

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

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


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

成员属性和方法的操作

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

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

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


a = Car()
b = Car()
print(a)
print(b)

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


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

对象的成员操作

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


res = a.color
print(res)

---
black


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


a.rode()

---
小汽车能代步。


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


a.color = 'red'
res = a.color
print(res)

---
red


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


print(b.color)

---
black


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

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

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


a.name = 'AE86'
print(a.name)

---
AE86


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


print(b.name)

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


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

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


print(a.name)
del a.name
print(a.name)

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


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

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


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对象内创建的属性,因此可以删除。

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

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

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

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

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

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


a.rode()

---
小汽车能代步


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


def func():
print('这里是重新定义的一个方法')

a.rode = func
a.rode()

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


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

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


a.func2 = func
a.func2()

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


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

来,删除一下方法试试:


del a.func2


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


a.func2()

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


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

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

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

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

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

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

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


Car.brand = 'BMW'


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


print(a.brand) # 先执行一次打印,原始属性
Car.brand = 'BMW'
b = Car() # 新创建一个实例化对象
print(b.brand) # 打印新创建的对象的属性
print(a.brand) # 打印修改之前创建的对象的属性

---
mustang
BMW
BMW


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

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

对象操作成员


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

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


类操作成员(不推荐)


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

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


最终总结一下如下:

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

成员方法中的self

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

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

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


# 定义人
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到底是什么:


# 定义人
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来试试看:


# 定义人
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就可以代表对象去完成,比如成员的添加、删除、更新、访问、调用等等。

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


# 定义人
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这个实例化对象。

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


Person.func()

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


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

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


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,而是其他的参数是否可以成功:


class Person():
def func(vars):
print(f'我是{vars.name}, 我使用了vars来接受参数。')

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

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


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

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

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

魔术方法

魔术方法是什么呢?

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

__init__ 初始化方法

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


class Person():
name = None
age = None
sex = None

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

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

# 实例化对象
zs = Person()

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


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

  • __init__ 触发机制: 在通过类实例化对象后,自动触发的一个方法
  • 作用:可以在对象实例化之后完成对象的初始化(属性的复制,方法的调用)。
  • 应用场景:文件的打开,数据的获取。 干活之前,做好一些准备工作。

以下,我们改造一下这个类,然后再实例化的时候多做一些动作:


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方法,完成自我介绍:


def __init__(self, name, age, sex):
...
self.say()

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


__del__: 析构方法

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

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

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

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


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

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


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 => 私有的。

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


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成员属性是属于类的。

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


print(zs._age)
print(zs.__sex)

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


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

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


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中的私有化封装是通过改名策略实现的,并不是真正的私有化

继承

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

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

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

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

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

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


# 继承的语法格式
class 父类():
pass

class 子类(父类):
pass


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


# 定义猫科动物
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中完全不需要再次输入这么多,完全可以这样写:


# 定义猫科动物
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()方法。

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


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().父类方法名()来进行操作:


# 定义猫科动物
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类。。。

单继承和多继承

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


class Person():
print('人的样子。')

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

class Japanese(Person, Chusheng):
pass

c = Japanese()
c

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


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


# 单继承
class 父类():
pass

class 子类(父类):
pass

# 多继承
class ():
pass

class ():
pass

class (父,母):
pass


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


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方法呢?

让我们看打印结果:


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


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


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()

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


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

菱形继承(钻石继承)

先来看一个图形:


    A
B C
D


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

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

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

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


# 菱形继承

# 祖先
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
居然退化了,又忘了怎么吃...


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


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


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

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


# 菱形继承

# 祖先
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了?我们来看看原因:


'''
在定义类之后,程序会自动生成一个继承的列表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()类关系检测

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


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接口来说。就属于多态。

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


# 定义电脑类
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代表了不同的实例化对象,那也就会启动不同的类方法。

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

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


# 继承关系写多态

# 定义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方法,也就是继承了规范。

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

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

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

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

茶桁
茶桁
80后,先后在多家大厂担任数据产品经理,中台产品。
留言0
查看全部
發表第一個留言支持創作者!