3.6 面向对象基础
20世纪60年代,人们首次提出面向对象(Object Oriented,OO)的概念,发展至今已成为一种成熟的编程思想,并逐步成为软件开发领域的主流技术。面向对象编程(Object Oriented Programming,OOP)主要针对大型软件开发,可以使软件设计更加灵活,并且能更好地进行代码复用。
面向对象中的对象(Object)通常是指客观世界中存在的对象,具有唯一性,对象之间各不相同,各有各的特点,每一个对象都有自己的运动规律和内部状态;对象与对象之间可以相互联系,相互作用。另外,对象也可以是一个抽象的事物,例如,可以从圆形、正方形、三角形等图形抽象出一个简单图形,简单图形就是一个对象,它有自己的属性和行为,图形中边的个数是它的属性,图形的面积也是它的属性,输出图形的面积就是它的行为。概括地讲,面向对象技术是一种从组织结构上模拟客观世界的方法。
3.6.1 面向对象概述
1.对象
世间万物皆对象,现实世界中的任意一个事物都是对象。对象是事物存在的实体,如一个人,如图3.56所示。
通常将对象划分为两个部分,即静态部分与动态部分。静态部分被称为“属性”,任何对象都具备自身属性,这些属性不仅是客观存在的,而且是不能被忽视的,如人的性别,如图3.57所示;动态部分指的是对象的行为,即对象可执行的动作,如人可以跑步,如图3.58所示。
图3.56 对象“人”的示意图
图3.57 静态属性“性别”示意图
图3.58 动态属性“跑步”示意图
说明
Python中一切皆为对象,不仅具体的事物是对象,字符串、函数等也都是对象。这说明Python天生就是为面向对象而开发出来的程序语言。
2.类
类是封装对象的属性和行为的载体,反过来说具有相同属性和行为的一类实体被称为类。例如,把雁群比作大雁类,那么大雁类就具备了喙、翅膀和爪等属性,觅食、飞行和睡觉等行为,而一只要从北方飞往南方的大雁则被视为大雁类的一个对象。大雁类和大雁对象的关系如图3.59所示。
图3.59 大雁类和大雁对象的关系图
在Python语言中,类是一种抽象概念。如定义一个大雁类(Geese),在该类中可以定义每个对象共有的属性和方法;而一只要从北方飞往南方的大雁则是大雁类的一个对象(wildGeese),对象是类的实例。有关类的具体实现将在3.6.2节中详细介绍。
3.面向对象程序设计的特点
面向对象程序设计具有三大基本特征:封装、继承和多态。
1)封装
封装是面向对象编程的核心思想,将对象的属性和行为封装起来,其载体就是类。类通常会对客户隐藏其实现细节,这就是封装的思想。例如,用户使用计算机时,只需要使用手指敲击键盘就可以实现一些功能,并不需要知道计算机内部是如何工作的。
采用封装思想可保证类内部数据结构的完整性,使用该类的用户不能直接看到类中的数据结构,而只能执行类允许公开的数据,这样就避免了外部对内部数据的影响,提高了程序的可维护性。
使用类实现封装特性如图3.60所示。
图3.60 封装特性示意图
2)继承
矩形、菱形、平行四边形和梯形都是四边形,它们具有共同的特征—拥有4条边。
只要将四边形适当地延伸,就会得到矩形、菱形、平行四边形和梯形4种图形。以平行四边形为例,如果把平行四边形看作四边形的延伸,那么平行四边形就复用了四边形的属性和行为,同时添加了平行四边形特有的属性和行为,如平行四边形的对边平行且相等的特性。在Python中,可以把平行四边形类看作是继承四边形类后产生的类,其中,四边形类称为父类或超类,平行四边形类称为子类。值得注意的是,可以说平行四边形是特殊的四边形,但不能说四边形是平行四边形。同理,Python中可以说子类的实例都是父类的实例,但不能说父类的实例是子类的实例。四边形类层次结构示意图如图3.61所示。
综上所述,继承是实现重复利用的重要手段,子类通过继承复用了父类的属性和行为,同时又添加了子类特有的属性和行为。
3)多态
各个子类在继承父类特征的基础上,增加自己独有的新特征,将各自区别开来的状态,就称为多态。比如创建一个螺丝类,螺丝类有两个属性:粗细和螺纹密度;然后再创建了两个类,一个是长螺丝类,一个是短螺丝类,并且它们都继承了螺丝类。这样长螺丝类和短螺丝类不仅具有相同的特征(粗细相同,且螺纹密度也相同),还具有不同的特征(一个长,一个短,长的可以用来固定大型支架,短的可以固定生活中的家具)。综上所述,一个螺丝类衍生出不同的子类,子类继承父类特征的同时也具备了自己的特征,并且能够实现不同的效果,这就是多态。螺丝类层次结构示意图如图3.62所示。
图3.61 四边形类层次结构示意图
图3.62 螺丝类层次结构示意图
3.6.2 类的定义和使用
在Python中,类表示具有相同属性和方法的对象的集合。在使用类时,需要先定义类,然后再创建类的实例,通过类的实例就可以访问类中的属性和方法。
1.定义类
Python中,类的定义使用class关键字来实现,语法如下:
class ClassName: '''类的帮助信息''' #类文档字符串 statement #类体
参数说明:
ClassName:用于指定类名,一般使用大写字母开头。如果类名中包括两个单词,第二个单词的首字母也大写,这种命名方法也称为“驼峰式命名法”。当然,也可根据自己的习惯命名,但是一般推荐按照惯例来命名。
'''类的帮助信息''':用于指定类的文档字符串,定义该字符串后,在创建类的对象时,输入类名和左侧的括号“(”后,将显示该信息。
statement:类体,主要由类变量(或类成员)、方法和属性等定义语句组成。如果在定义类时,没想好类的具体功能,也可以在类体中直接使用pass语句代替。
例如,下面以大雁为例声明一个类,代码如下:
class Geese: '''大雁类''' pass
2.创建类的实例
定义完类后,并不会真正创建一个实例。类的定义有点像汽车的设计图纸,可以告诉你汽车长什么样,但它本身并不是一个汽车,你不能驾驶它,却可以用它来建造真正的汽车,而且可以用来制造很多汽车。
如何创建实例呢?class语句本身并不创建该类的任何实例。所以在类定义完成以后,可以创建类的实例,即实例化该类的对象。创建类的实例的语法如下:
ClassName(parameterlist)
其中,ClassName是必选参数,用于指定具体的类;parameterlist是可选参数,当创建一个类时,没有创建__init__()方法,或者__init__()方法只有一个self参数时,parameterlist可以省略。
例如,创建上文定义的Geese类的实例,可以使用下面的代码:
wildGoose = Geese() #创建大雁类的实例 print(wildGoose)
执行上面的代码,将显示类似下面的内容:
<__main__.Geese object at 0x0000000002F47AC8>
从上面的执行结果中可以看出,wildGoose是Geese类的实例。
3.创建__init__()方法
创建完一个类后,需要为其创建__init__()方法。该方法类似于Java语言中的构造方法,每当创建类的新实例时,Python都会自动执行它。__init__()方法必须包含一个self参数,并且必须是第1个参数。self参数是一个指向实例本身的引用,用于访问类中的属性和方法。在方法调用时会自动传递实际参数self,因此当__init__()方法只有一个参数时,创建类的实例时将不再需要指定实际参数。
说明
在__init__()方法的名称中,开头和结尾处是两个下画线(中间没有空格),这是一种约定,旨在区分Python的默认方法和普通的方法。
下面仍然以大雁为例,声明一个Geese类,并且创建__init__()方法,代码如下:
运行上面的代码,将输出以下内容:
我是大雁类!
从运行结果可以看出,创建大雁类实例时虽然没有为__init__()方法指定参数,该方法仍会自动执行。
在__init__()方法中,除self参数外,还可以自定义一些参数,参数间使用逗号“,”进行分隔。例如,下面的代码将在创建__init__()方法时再指定3个参数,分别是beak、wing和claw。
执行上面的代码,将显示如图3.63所示的运行结果。
图3.63 创建__init__()方法时指定4个参数
4.创建类的成员并访问
类的成员主要由实例方法和数据成员组成。在类中创建了类成员后,可以通过类的实例进行访问。
1)创建实例方法并访问
所谓实例方法,就是在类中定义的函数,该函数需要在类的实例上操作。同__init__()方法一样,实例方法的第1个参数必须是self,并且必须包含一个self参数。创建实例方法的语法格式如下:
def functionName(self,parameterlist): block
参数说明:
functionName:用于指定方法名,一般使用小写字母开头。
self:必要参数,表示类的实例,其名称可以是self以外的单词。
parameterlist:用于指定除self参数外的参数,各参数间使用逗号“,”进行分隔。
block:方法体,表示要实现的具体功能。
说明
实例方法和Python中函数的主要区别就是:函数实现的是某个独立的功能,而实例方法用于实现类中的某个行为,是类的一部分。
实例方法创建完成后,可以通过类的实例名称和点操作符(.)进行访问,语法格式如下:
instanceName.functionName(parametervalue)
参数说明:
instanceName:类的实例名称。
functionName:要调用的方法名称。
parametervalue:对应的实际参数,其数量比parameterlist的个数少1。
【实例3.19】 创建大雁类并定义飞行方法。(实例位置:资源包\Code\03\19)
具体代码如下:
运行结果如图3.64所示。
图3.64 创建大雁类并定义飞行方法
多学两招
在创建实例方法时,也可以像创建函数一样为参数设置默认值。但是被设置了默认值的参数必须位于所有参数的最后面(即最右侧)。例如,可以将实例3.19的第8行代码修改为以下内容:
def fly(self, state = "我会飞行"):
此时再调用该方法,就可以不再指定参数值,第15行代码可修改为“wildGoose.fly()”。
2)创建数据成员并访问
数据成员是指在类中定义的变量,即属性。根据定义位置,又可以分为类属性和实例属性。
类属性:定义在类中,并且在函数体外的属性。类属性可以在类的所有实例之间共享值,也就是在所有实例化的对象中公用。类属性可以通过类名或者实例名进行访问。
例如,定义一个大雁类Geese,在该类中定义3个类属性,用于记录雁类的特征,代码如下:
创建上面的类Geese,然后创建该类的实例,代码如下:
geese = Geese() #实例化一个雁类的对象
应用上面的代码创建完Geese类的实例后,将显示以下内容:
我属于雁类!我有以下特征: 脖子较长 振翅频率高 腿位于身体的中心支点,行走自如
下面通过一个具体的实例演示类属性在类的所有实例之间共享值的应用。
【实例3.20】 创建大雁类并共享类属性。(实例位置:资源包\Code\03\20)
定义一个大雁类Geese,并在该类中定义4个类属性,前3个用于记录大雁类的特征,第4个用于记录实例编号,然后定义一个构造方法,在该构造方法中将记录实例编号的类属性进行加1操作,并输出4个类属性的值,最后通过for循环创建4个大雁类的实例,代码如下:
运行结果如图3.65所示。
图3.65 通过类属性统计类的实例个数
Python中除了可以通过类名称访问类属性,还可以动态地为类和对象添加属性。例如,在实例3.20的基础上为雁类添加一个beak属性,并通过类的实例访问该属性,可以在上面代码的后面再添加以下代码:
01 Geese.beak = "喙的基部较高,长度和头部的长度几乎相等" #添加类属性 02 print("第2只大雁的喙:",list1[1].beak) #访问类属性
说明
上面的代码只是以第2只大雁为例进行演示,读者也可以换成其他的大雁试试。运行后,将在原来的结果后面再显示以下内容:
第2只大雁的喙:喙的基部较高,长度和头部的长度几乎相等
除了可以动态地为类和对象添加属性外,也可以修改类属性,修改结果将作用于该类的所有实例。
实例属性:指定义在类的方法中的属性,只作用于当前实例。
例如,定义一个雁类Geese,在该类的__init__()方法中定义3个实例属性,用于记录雁类的特征,代码如下:
创建上面的类Geese,然后创建该类的实例,代码如下:
geese = Geese() #实例化一个雁类的对象
应用上面的代码创建Geese类的实例后,将显示以下内容:
我属于雁类!我有以下特征: 脖子较长 振翅频率高 腿位于身体的中心支点,行走自如
对于实例属性也可以通过实例名称修改,与类属性不同,通过实例名称修改实例属性后,并不影响该类的另一个实例中相应的实例属性的值。例如,定义一个雁类,并在__init__()方法中定义一个实例属性,然后创建两个Geese类的实例,并且修改第一个实例的实例属性,最后分别输出这两个实例的实例属性,代码如下:
运行上面的代码,将显示以下内容:
脖子较长 脖子较长 goose1的neck属性: 脖子没有天鹅的长 goose2的neck属性: 脖子较长
5.访问限制
在类内定义的属性和方法,在类外可以直接调用,从而隐藏类内部的复杂逻辑。Python并没有对属性和方法的访问权限进行限制。为了保证类内部的某些属性或方法不被外部所访问,可以在属性或方法名前面添加单下画线(_foo)、双下画线(__foo)或首尾加双下画线(__foo__),从而限制访问权限。
首尾双下画线表示定义特殊方法,一般是系统定义名字,如__init__()。
以单下画线开头的表示protected(保护)类型的成员,只允许类本身和子类进行访问,但不能使用“from module import *”语句导入。
例如,创建一个Swan类,定义保护属性_neck_swan,并使用__init__()方法访问该属性,然后创建Swan类的实例,并通过实例名输出保护属性_neck_swan,代码如下:
执行上面的代码,将显示以下内容:
__init__(): 天鹅的脖子很长 直接访问: 天鹅的脖子很长
从上面的运行结果可以看出,保护属性可以通过实例名访问。
双下画线表示private(私有)类型的成员,只允许定义该方法的类本身进行访问,而且也不能通过类的实例进行访问,但是可以通过“类的实例名._类名__xxx”方式访问。
例如,创建一个Swan类,定义私有属性__neck_swan,并使用__init__()方法访问该属性,然后创建Swan类的实例,并通过实例名输出私有属性__neck_swan,代码如下:
执行上面的代码后,将输出如图3.66所示的结果。
图3.66 访问私有属性
从上面的运行结果可以看出,私有属性不能直接通过实例名+属性名访问,可以在类的实例方法中访问,也可以通过“实例名._类名__xxx”的方式访问。