脚本语言系列之Python | Python面向对象
21. 面向对象及特性
1 面向对象思想
面向对象编程(Object Oriented Programming),简称为OOP,是一种以对象为中心的程序设计思想。与之相对的,就是面向过程编程(Procedure Oriented Programming), 简称为POP, 是一种以过程为中心的程序设计思想。
这两个编程思想到底有什么不同?
面向对象编程(Object Oriented Programming,简称OOP),是利用类和对象来创建各种模型来实现对真实世界的描述,使用面向对象编程的原因不仅因为它可以使程序的维护和扩展变得更简单,并且可以大大提高程序开发效率 ,另外,基于面向对象的程序可以使它人更加容易理解你的代码逻辑,从而使团队开发变得更从容。
1.1 把大象装进冰箱分几步
考虑一个例子:把大象装进冰箱分几步?
第一步:打开冰箱门
第二步:把大象装进去
第三步:关上冰箱门
这个答案,就是一种面向过程的思维,遇到问题之后,分析解决问题的步骤,然后一步步的去实现。
如果是面向对象的话,又该如何去做?
面向对象是通过分析问题中需要的抽象模型,然后根据需要的功能分别去创建模型对象,最终由模型对象来完成程序。那这个把大象装进冰箱分几步的问题我们该如何去考虑呢?
首先,面向对象要解决这个问题,需要先建立出抽象模型,比如:
(1)打开冰箱门和关闭冰箱门,这都属于一个冰箱的功能。
(2)大象走进去,这就是大象的功能。
到此时我们就出现了两个抽象模型,一个是冰箱,一个是大象。
冰箱具有打开和关闭的功能,大象具有走路的能力。
分析到这里,就是面向对象的思想,具体完成的话,就是去创建冰箱和大象这两个对象,最终完成这个程序。
冰箱对象-开门,大象对象-走进冰箱,冰箱对象-关门。
1.2 想吃清蒸鱼怎么办
思考一个新的问题:想吃清蒸鱼怎么办?
当然是按照做菜的顺序一步一步来对吧?这就是典型的面向过程思维:
(1)买鱼,买料
(2)杀鱼和清理,并且腌制
(3)锅里烧水
(4)把鱼放进去,开始蒸鱼。
(5)十分钟后开盖,把鱼端出来,然后浇汁。
这样,一步一步的完成这个愿望,就是面向过程所作的事情。
轮到面向对象,又该如何呢?
(1)需要一个对象:大厨。
(2)告诉大厨,我想吃清蒸鱼。
那么大厨呢,有可能是我们自己训练的,也有可能是其他五星酒店挖过来的。不管如何,这是一个已经完善建立好的对象,我们直接拿来用就可以了。面向对象呢,就是这样寻找具体的对象去解决问题。对于我们来说,调用了对象,而对象完成了这个过程。
当然,具体大厨这个对象里肯定还是一步一步的去完成过程,也就是说,最终面向对象中是由面向过程的体现的。但是思维方式,也就是设计思想是完全不同的。
1.3 优缺点
面向过程的核心是过程,过程就是指解决问题的步骤。其优缺点非常明显:
(1)优点: 将负责的问题流程化,进而实现简答。
(2)缺点: 扩展性差(更新、维护、迭代)。
而面向对象的核心是对象,是一个特征和功能的综合体,其优缺点如下:
(1)优点:可扩展性高。
(2)缺点:编程复杂度相对面向过程高一些,这里的复杂度指的是计算机在执行面向对象的程序时性能表现一般。
那总结起来呢,在去完成一些简单的程序时,可以使用面向过程去解决。但是如果有复杂的程序或任务,而且需要不断的进行迭代和维护,那么肯定是优先选择面向对象的编程思想。
如何去学习面向对象编程呢?其实就两步:
(1)学习面向对象编程的思想。
(2)学习面向对象编程的语法。
这两步中,其实难的是第一步,学习面向对象编程的思想。
1.4 面向对象和面向过程
一、面向对象(Object Oriented,简称OO),是一种程序设计思想,如python和java语言就是一种面向对象的编程语言:
(1)OOP把对象作为程序的基本单元,一个对象包含了数据和操作数据的函数。
(2)OOP把程序看做不同对象的相互调用,OOP的抽象程度比函数要高。
二、面向过程(Procedure Oriented简称PO),也是一种常见的程序设计思想,如c语言:
(1)面向过程的程序设计的核心是过程(流水线式思维),过程即解决问题的步骤,面向过程的设计就好比精心设计好一条流水线,考虑周全什么时候处理什么东西。
(2)面向过程编程是把函数看着程序的最基本单元,一个函数包括要处理的数据及算法逻辑。
(3)面向过程编程是把程序看作不同函数之间的互相调用。
(3)面向过程编程的抽象层度相对较低。
优点是:极大的降低了写程序的复杂度,只需要顺着要执行的步骤,堆叠代码即可。
缺点是:一套流水线或者流程就是用来解决一个问题,代码牵一发而动全身。
1.5 面向对象的常见概念
(1)类(Class): 用来描述具有相同的属性和方法的对象的集合。它定义了该集合中每个对象所共有的属性和方法,对象是类的实例。
(2)类变量:类变量在整个实例化的对象中是公用的,类变量定义在类中且在函数体之外。
(3)局部变量:定义在方法中的变量,只作用于当前实例的类。
(4)实例变量:在类的声明中,类属性是用变量来表示的。这种变量就称为实例变量,一般使用self.variableName。
(5)实例化:创建一个类的实例,类的具体对象。
(6)方法:类中定义的函数,类中的方法必须有一个参数self,也必须是在位置参数的第一位。
(7)对象:通过类定义的数据结构实例。对象包括两个数据成员(类变量和实例变量)和方法。
(8)方法重写:如果从父类继承的方法不能满足子类的需求,可以对其进行改写,这个过程叫方法的覆盖(override),也称为方法的重写。
2 类的创建及调用
类: 类是对象的一个抽象的概念。
对象(实例):对象就是由类创建的实例。
那么这两者的关系其实就是【模具和铸件】之间的关系。
类是由对象总结而来的,总结的这个过程叫做抽象。
对象是由类具体实施出来的,这个过程叫做实例化。
思考下面的问题:
水果是一个对象还是一个类?
汽车是一个对象还是一个类?
在说【水果】的时候,你能想到什么?香蕉、苹果、西瓜、榴莲等。想了这么多不一样的东西,是不是这些所有的都称为是【水果】?那么将这些内容都叫做水果的过程就称为【归类】的过程。这个【水果】就是一个类,刚才我们总结的这个过程就叫做抽象,我们想到的香蕉、苹果…等等,就是对象。
汽车其实是一个概念,你能想到什么?奔驰、野马、奥迪?我们见过的车,就会在我们脑海中浮现,而这些具体的车总结出来一个类的过程就是【抽象的过程】,我们最后总结出来的【汽车】就是一个类。那些在我们脑海里浮现的具体的某一款汽车,就是对象。
2.1 类的创建
使用class关键字来创建一个新类,class之后为类的名称()并以冒号结尾:
class ClassName():
'''类的帮助信息'''
类体,包括类的变量和方法
下面写一个动物类的案例:
class Animal():
#这些都是类变量,在类中,方法外
nicheng = "动物"
#类中的方法参数中必须要有一个self,而且必须是在位置参数的第一位
#实例(对象)变量,在变量前需要有一个self.
def info(self):
self.age = 0
self.gender = "male"
self.weight = 0
self.brand = "xxx"
def eat(self):
print("站着吃")
def sleep(self):
print("趴着睡")
2.2 类对象的创建及使用
A=className()
类对象支持两种操作:属性引用和方法引用
标准语法:obj.name
针对上文动物类的对象创建及使用:
# 创建对象pig,及使用对象的变量和方法
pig = Animal()
pig.sleep()
pig.eat()
print(Animal.nicheng)
pig.info()
print(pig.age)
2.3 类变量的使用
class Person():
# 国籍定义为类变量比较好
guoji = "中国"
# 创建了小王的对象
xiaowang = Person()
print(Person.guoji) # 通过类访问
print(xiaowang.guoji) # 通过对象访问
# 如果通过xiaowang调用类变量进行赋值,则此处创建了一个与类变量同名的实例变量,
# 修改的也是该对象的实例变量值,而不是类变量值
xiaowang.guoji = "英国"
print(Person.guoji) # 通过类访问
print(xiaowang.guoji) # 通过对象访问
# 如果通过类名调用类变量进行赋值,此处修改的是类变量值,其他新对象的该值也改变了
Person.guoji = "法国"
print(xiaowang.guoji)
xiaoli = Person()
print(xiaoli.guoji)
2.4 构造方法的使用
(1)构造方法可以实现对实例变量的初始化操作。
(2)init的特殊方法(构造方法),在类实例化时会自动调用。
(3)init方法可以有参数,参数通过 init传递到类的实例化操作上。
(4)可以不显示地写init方法,会默认使用无参数的构造方法。
(5)如果显示地写了构造方法,则不能再使用无参数的构造方法。
class studentInit():
#声明类变量
type1 = '学生'
# 通过构造方法实现对实例变量的初始化
def __init__(self,age,name,ID):
# 定义并对实例变量进行初始化
self.age = age
self.name = name
self.ID = ID
# 定义第一个方法info,实现对实例变量的显示
def info(self): # 显示对象属性的方法
print("年龄是{}、姓名是{}、学号是{}".format(self.age,self.name,self.ID))
def study(self):
score=80
self.ID = "0000000"
print("学生{}的学习成绩是{}".format(self.ID,score))
def play(self):
print("学习之余需要玩会游戏!")
#创建类的对象(实例化:由抽象到具体)
#如果你显示地写了带参数的构造方法,则不再允许使用默认的无参数的构造方法
xiaohua = studentInit(20,"xiaohua","12345678")
print(xiaohua.type1)
print(xiaohua.age)
xiaohua.info()
xiaohua.play()
xiaohua.study()
2.5 self的使用
(1)类的方法与普通的函数只有一个特别的区别——它们必须有一个额外的第一个参数名称, 按照惯例它的名称是self。
(2)self不是关键字,你可以把它改成其他名称,虽然代码能正常运行,但是还是会提示:Method should have self as first argument。
(3)self代表的是类的实例,代表当前对象的地址,而self.__class__则指向类。
class Person():
def ppp(self):
print(self)
print(self.__class__)
# 下面两个输出的是同一个地址空间
# 说明self就是类的具体实例,此处就是指的小王
xiaowang = Person()
print(xiaowang)
xiaowang.ppp()
输出如下:
<__main__.Person object at 0x0000015009306070>
<__main__.Person object at 0x0000015009306070>
<class '__main__.Person'>
3 面向对象的三大特性
三大特性包括:
封装
继承
多态
3.1 封装
对于私有的属性,不能被外界使用,但不可避免的要被访问和修改,我们就提供一些方法进行修改和对外访问接口,这种方式叫做封装 - 将变化隔离 - 提高复用性 - 提高安全性。
3.1.1 变量私有化
在python中用双下划线开头的方式将属性隐藏起来(设置成私有的)。
(1)__private_attrs:两个下划线开头,声明该属性为私有,不能在类的外部直接访问,在类内部的方法中使用时self.private_attrs。
(2)__private_method:两个下划线开头,声明该方法为私有方法,只能在类的内部调用self.private_methods,不能在类的外部调用。
class Student():
def __init__(self,name,age,sex):
self.__name = name
self.__age = age
self.__sex = sex
one = Student('allen',18,'男')
# print(one.__name) # 报错,无该属性
# print(one.name) # 报错,无该属性
print(one._Student__name)
print(one._Student__age)
print(one._Student__sex)
one._Student__age = 20
print(one._Student__age)
此时发现,我们虽然不能使用one.__name或者one.name访问到该属性。但是我们可使用one._Student__age访问到对象的age属性并且能修改。说明python在设置私有属性的时候,只是把属性的名字换成了其他的名字。
3.1.2 提供对外访问
将变量私有化后,还需要对外提供公共的访问方式:
class Student():
def __init__(self,name,age,sex):
self.__name = name
self.__age = age
self.__sex = sex
def info(self):
print("姓名:{}年龄:{}性别:{}".format(self.__name,self.__age,self.__sex))
def get_name(self):
return self.__name
def set_name(self,name):
if len(name) > 1 :
self.__name = name
else:
print("name的长度必须要大于1个长度")
def get_age(self):
return self.__age
def set_age(self, age):
if age > 0 and age < 150:
self.__age = age
else:
print("输入的年龄必须要大于0,小于150岁")
one = Student('plf',18,'男')
one.info()
one.set_name('a') # 通过自己设置接口,可以有效规避脏数据
print(one.get_name()) # 通过接口获取数据
one.set_age(-9) # 通过自己设置接口,可以有效规避脏数据
print(one.get_age()) # 通过接口获取数据
3.1.3 装饰器
除此之外还可以使用@property 装饰器提供私有数据的访问。
class Student():
def __init__(self,name,age,sex):
self.__name = name
self.__age = age
self.__sex = sex
def info(self):
print("姓名{}年龄{}性别{}".format(self.__name,self.__age,self.__sex))
@property
def name(self):
return self.__name
@name.setter
def name(self,name):
if len(name) > 1 :
self.__name = name
else:
print("name的长度必须要大于1个长度")
@property
def age(self):
return self.__age
@age.setter
def age(self, age):
if age > 0 and age < 150:
self.__age = age
else:
print("输入的年龄必须要大于0,小于150岁")
one = Student('plf',18,'男')
one.info()
one.name = '张三'
print(one.name)
one.age = 170
print(one.age)
3.2 继承
继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。
继承,其实这样理解,就是我写了一个爸爸类和儿子类,爸爸有钱,儿子却没钱,于是儿子决定继承爸爸,调用爸爸的钱(爸爸类的变量和方法)。
(1)面向对象的编程带来的主要好处之一是代码的重用,通过继承机制实现。
(2)通过继承创建的新类称为子类或派生类,被继承的类称为基类、父类或超类。
(3)继承的实现必须基于类的“继承”关系。
继承语法是:
class 派生类名(基类名):
类的代码
...
3.2.1 继承示例
# 引入继承的理念:
# 提取不同类中共有的属性和行为:年龄、性别、颜色、吃鱼、睡、玩
# 把提取的属性和行为定义到一个动物类中:
class animal(): # 父类
def __init__(self,age,sex,color):
self.age = age
self.sex = sex
self.color = color
def eat(self):
print("吃饭")
def sleep(self):
print("睡觉")
def play(self):
print("玩")
#可以使用继承的方式来写子类
class cat(animal):#子类
#重写:前提是必须有继承关系,父类的行为在子类中不一定全部通用
#子类有自己的特性,那就把父类的行为重写一下
#方法名保持一致,参数无所谓
def eat(self,food):
print("猫吃鱼")
#创建一只小猫
c = cat(1,"male","yellow")
print(c.color)
c.eat("鱼")
c.play()
3.2.2 调用基类方法
在python中继承中的一些特点:
(1)如果在子类中需要父类的构造方法就需要显式的调用父类的构造方法,或者不重写父类的构造方法。
(2)在调用基类的方法时,需要加上基类的类名前缀,且需要带上self参数变量。
class Parent(): # 定义父类
parentAttr = 100
def __init__(self):
print("调用父类构造函数")
def parentMethod(self):
print('调用父类方法')
class Child(Parent): # 定义子类
def __init__(self):
print("调用子类构造方法")
def setAttr(self, attr):
Parent.parentAttr = attr
def getAttr(self):
print("父类属性 :", Parent.parentAttr)
def childMethod(self):
Parent.parentMethod(self)
print('调用子类方法')
c = Child() # 实例化子类
c.childMethod() # 调用子类的构造方法
c.parentMethod() # 调用父类方法
c.setAttr(200) # 调用子类方法修改父类属性 - 设置属性值
c.getAttr() # 调用子类方法查看父类属性 - 获取属性值
3.2.3 方法重写
方法重写,如果你的父类方法的功能不能满足你的需求,你可以在子类重写你父类的方法:
class Parent(): # 定义父类
def myMethod(self):
print('调用父类方法')
class Child(Parent): # 定义子类
pass
# def myMethod(self):#字类重写父类的同名方法
# print('调用子类方法')
c = Child() # 子类实例
c.myMethod() # 子类调用重写方法,如果不重写,则调用父类方法
3.3 多态
Java中多态性,可以理解为一个事物的多种形态,比如我们说猫是动物、猫也是猫,猫就具备了不同的形态。
同样python中也支持多态,但是是有限的的支持多态性,主要是因为python中变量的使用不用声明,所以不存在父类引用指向子类对象的多态体现,同时python不支持重载。
在python中 多态的使用不如Java中那么明显,所以python中刻意谈到多态的意义不是特别大。
4 举个例子
如果我们需要实例一个对象,那么我们就需要先抽象一个类。
(1)首先,我们需要抽象一个汽车类,也就是我们要在一个设计图纸上设计处这个汽车。
然后,我们由这个设计图纸去创建(实例)出来的真实汽车就是一个对象。
一个类需要有【特征】和【功能】两个内容组成:
【特征】在编程中就是一个变量,在类中称为属性。
【功能】在编程中就是一个函数,在类中称为方法。
# 定义一个汽车的类
class Car():
# 属性 => 特征 => 变量
color = 'black' # 表示颜色属性
brand = 'mustang' #表示品牌属性
displacement = 2.4 # 表示排量属性
# 方法 => 功能 => 函数
def pulling(self):
print('小汽车能拉货。')
def rode(self):
print('小汽车能代步。')
def onDuty(self):
print('小汽车能上班。')
buyNewCar = Car()
print(buyNewCar.color)
buyNewCar.rode()