第7章 面向对象程序设计

7.1 面向对象概述

面向对象(Object Oriented)的英文缩写是OO,它是一种设计思想。从20世纪60年代提出面向对象的概念到现在,它已经发展成为一种比较成熟的编辑思想,并且逐步成为目前软件开发领域的主流计技术。如我们经常听说的面向对象编程(Object Oriented Programming,即OOP)就是主要针对大型软件设计而提出的,它可以使软件设计更加灵活,并且能更好地进行代码复用。

面向对象中的对象(Object),通常是指客观世界中存在的对象,具有唯一性,对象之间各不相同,各有各的特点,么一个对象都有自己的运动规律和内部状态;对象与对象之间有时可以相互联系、相互作用。另外,对象也可以是一个抽象的事物,例如,可以从圆形、正方形、三角形等图形抽象出一个简单图形,简单图就是一个对象,它有自己的属性和行为,图形中边的个数是它的属性,图形的面积也是它的属性,输出图形的面积就是它的行为。概括地讲,面向对象技术是一种从组织结构上模拟客观世界的方法。

7.1.1 对象

对象,是一个抽象概念,英文称作“Object”,表示任意存在的事物。世间万物皆对象!现实世界中,随处可见的一种事物就是对象,对象是事物存在的实体,如一个人。

通常将对象划分为两个部分,即静态部分和动态部分。静态部分被称为“属性”,任何对象都具备自身属性,这些属性不仅是客观存在的,而且是不能被忽视的,如人的性别。动态部分指的是对象的行为,即对象执行的动作,如人可以跑步。

说明:在Python中,一切都是对象。即不仅是具体的事物称为对象,字符串、函数等也都是对象。这说明Python天生就是面向对象的。

7.1.2 类

类是封装对象的属性和行为的载体,反过来说具有相同属性和行为的一类实体被称为类。例如,把雁群比作大雁类,那么大雁类就具备了喙、翅膀和爪等属性,觅食、飞行和睡觉等行为,而一只要从北方飞往南方的大雁被视为大雁类的一个对象。大雁类和大雁对象的关系。

在Python语言中,类是一种抽象概念,如定义一个大雁类(Geese),在该类中,可以定义每个对象共有的属性和方法;而一只要从北方飞往南方的大雁则是大雁类的一个对象(wildGeese),对象是类的实例。有关类的具体实现将在7.2节进行详细介绍。

7.1.3 面向对象程序设计的特点

 面向对象程序设计具有三大基本特征:封装、继承和多态。

1. 封装

封装是面向对象编程的核心思想,将对象的属性和行为封装起来,其载体就是类,类通常会对客户隐藏其现实细节,这就是封装的思想。例如,用户使用计算机,只需要使用手机敲击键盘就可以实现一些功能,而不需要知道计算机内部是如何工作的。

采用封装思想保证了类内部数据结构的完整行,使用该类的用户不能这看到类中的数据结构,而只能执行类允许公开的数据,这样就避免了外部对内部数据的影响,提高了程序的可维护性。

2. 继承

矩形、菱形、平行四边形和梯形等都是四边形。因为四边形与它们具有共同的特征:拥有4条边。只要将四边矩形适当延伸,就会得到矩形、菱形、平行四边形和梯形4 种图形。以平行四边形为例,如果把平行四边形的延伸,那么平行四边形就复用了四边形的属性和行为,同时添加了平行四边形特有的属性和行为,如平行四边形的对边平行且相等。在Python中,可以把平行四边形类看作是继承四边形类后产生的类,其中,将类似于平行四边形的类称为子类,将类似于四边形的类称为父类和超类。值得注意的是,在阐述平行四边形和四边形的关系时,可以说平行四边形是特殊的四边形,但不能说四边形是平行四边形。同理,Python 中可以说子类的实例都是父类的实例,但不能说父类的实例是子类的实例。

综上所述,继承是实现重复利用的重要手段,子类通过继承复用了父类的属性和行为的同时又添加了子类特有的属性和行为。

3. 多态

将父类对象应用于子类的特征就是多态。比如创建一个螺丝类,螺丝类有两个属性:粗细和螺纹密度;然后在创建了两个类,一个是长螺丝类,一个短螺丝类,并且它们都继承了螺丝类。这样长螺丝类和短螺丝类不仅具有相同的特征(粗细形同,且螺纹密度也相同),还具有不同的特征(一个长,一个短,长的可以用来固定大型支架,短的可以固定生活中的家具)。综上所述,一个螺丝类衍生出不同的子类,子类继承父类特征的同时,也具备了自己的特征,并且能够实现不同的效果,这就是多态化的结构。

7.2 类的定义和使用

在Python 中,类表示具有相同属性和方法的对象的集合。在使用类时,需要先定义类,然后再创建类的实例,通过类的实例就可以访问类中的属性和方法了。

7.2.1 定义类

在Python 中,类的定义使用class 关键字来实现,语法如下:

class ClassName:
    '''类的帮助信息'''             # 类文档字符串
    statement                    # 类体

 

参数说明:

  • ClassName:用于指定类名,一般使用大写字母开头,如果类名中包括两个单词,第二个单词的首字母也大写,这种命名方法也称为“驼峰式命名法”,这是惯例。当然也可以根据自己的习惯命名,但是一般推荐按照惯例来命名。
  • '''类的帮助信息''':用于指定类的文档字符串,定义该字符串后,在创建类的对象是,输入类名和左侧的括号“(” 后,将显示该信息。
  • statement:类体,主要由类变量(或类成员)、方法和属性等定义语句组成。如果在定义类时,没想好类的具体功能,可可以在类体中直接使用pass 语句代替。

例如,下面以大雁为例声明一个类,代码如下:

class Geese:
    '''大雁类'''
    pass

 

7.2.2 创建类的实例

定义完类后,并不会真正的创建一个实例。这有点像一个汽车的设计图。设计图可以告诉你汽车看上去怎样,但设计图本身不是一个汽车。你不能开走它,它只能用来创造真正的汽车,而且可以使用它制造很多汽车。那么如何创建实例呢?

class语句本身不创建该类的任何实例。所以在类定义完成以后,可以创建类的实例,即实例化该类的对象。创建类的实例的语法如下:

ClassName(parameterlist)

 

其中,ClassName 是必选参数,用于指定具体的类;parameterlist 是可选参数,当创建一个类时,没有创建__init__()方法(该方法将在7.2.3 小节进行详细介绍),或者__init__() 方法只有一个self 参数时,parameterlist 可以省略。

class Geese:
    '''大雁类'''
    pass
wildGoose = Geese()             # 创建大雁类的实例
print(wildGoose)

 

执行上面的代码后,将显示类似下面的内容:

<__main__.Geese object at 0x028BD430>

 

从上面的执行结果中可以看出,wildGoose是Geese类的实例。

7.2.3 创建__init__() 方法

在创建类后,可以手动创建一个__init__() 方法。该方法是一个特殊的方法,类似Java 语言中的构造方法。每当创建一个类的新实例时,Python 都会自动执行它。__init__() 方法必须包含一个self 参数,并且必须是一个第一个参数。self 参数是一个指向实例本身的引用,用于访问类中的属性和方法。在方法调用时会自动传递实际参数self,因此当__init__() 方法只有一个参数时,在创建类的实例时,就不需要指定实际参数了。

说明:在__init__() 方法的名称中,开头和结尾处是两个下划线(中间没有空格),这是一种约定,旨在区分Python 默认方法和普通方法。

例如,下面仍然以大雁为例声明一个类,并且创建__init__() 方法,代码如下:

class Geese:
    '''大雁类'''
    def __init__(self):         #构造方法
        print('我是大雁类!')
wildGoose = Geese()             # 创建大雁类的实例

运行上面的代码,将输出以下内容:

我是大雁类!

 

从上面的运行结果可以看出,在创建大雁类的实例时,虽然没有为__init__() 方法指定参数,但是该方法会自动执行。

在__init__() 方法中,除了self 参数外,还可以自定义一些参数,参数间使用逗号“,” 进行分隔。

例如,下面的代码将在创建__init__()方法时,在指定3个参数,分别是beak、wing和claw。

lass Geese:
    '''大雁类'''
    def __init__(self,beak,wing,claw):        #构造方法
        print('我是大雁类!我有以下特征:')
        print(beak)             # 输出喙的特征
        print(wing)             # 输出翅膀的特征
        print(claw)             # 输出爪子的特征
beak_1 = "喙的基本较高,长度和头部的长度几乎相等"  # 喙的特征
wing_1 = "翅膀长而尖"           # 翅膀的特征
claw_1 = "爪子是蹼状的"         # 爪子的特征
wildGoose = Geese(beak_1,wing_1,claw_1)             # 创建大雁类的实例

 

执行上面的代码,结果如下。

我是大雁类!我有以下特征:
喙的基本较高,长度和头部的长度几乎相等
翅膀长而尖
爪子是蹼状的

7.2.4 创建类的成员并访问

类的成员主要有实例方法和数据成员组成。在类中创建了类的成员后,可以通过类的实例进行访问。

1. 创建实例方法并访问

所谓实例方法是指在类中定义的函数。该函数是一种在类的实例上操作的函数。同__init__() 方法一样,实例方法的第一个参数必须是self,并且必须包含一个self 参数。创建实例方法的语法格式如下:

def functionName(self,parameterlist):
    block

 

参数说明:

  • functionName:用于指定方法名,一般使用小写字母开头。
  • self:必要参数,表示类的实例,其名称可以是self 以外的单词,使用self 只是一个惯例而已。
  • parameterlist:用于指定出self 参数以外的参数,各参数间使用逗号“,” 进行分隔。
  • block:方法体,显示的具体功能。

说明:实例方法和Python 中的函数的主要区别就是,函数实现的是某个独立的功能,而实例方法是显示类中的一个行为,是类的一部分。

 实例方法创建完成后,可以通过类的实例名称和点(.)操作符进行访问,语法格式如下:

intanceName.functionName(parametervalue)

 

参数说明:

  • instanceName:为类的实例名称。
  • functionName:为要调用的方法名称。
  • parametervalue:表示为方法指定对应的实际参数,其值的个数与创建实例方法中parameterlist的个数相同。

实例01:创建大雁并定义飞行方法

class Geese:
    '''大雁类'''
    def __init__(self,beak,wing,claw):        #构造方法
        print('我是大雁类!我有以下特征:')
        print(beak)             # 输出喙的特征
        print(wing)             # 输出翅膀的特征
        print(claw)             # 输出爪子的特征
    def fly(self,state):        # 定义飞行方法
        print(state)
'''****************调用方法***************'''    
beak_1 = "喙的基本较高,长度和头部的长度几乎相等"  # 喙的特征
wing_1 = "翅膀长而尖"           # 翅膀的特征
claw_1 = "爪子是蹼状的"         # 爪子的特征
wildGoose = Geese(beak_1,wing_1,claw_1)             # 创建大雁类的实例
wildGoose.fly("我飞行的时候,一会儿排成个人字,一会儿排成个一字")  # 调用实例方法

 

运行结果

我是大雁类!我有以下特征:
喙的基本较高,长度和头部的长度几乎相等
翅膀长而尖
爪子是蹼状的
我飞行的时候,一会儿排成个人字,一会儿排成个一字

 

多学两招:在创建实例方法时,也可以和创建函数是一样为参数设置默认值。但是被设置了默认值的参数必须位于所有参数的最后(即最右侧)。例如,可以将实例01的第8行代码修改为以下内容:

def fly(self,state = '我会飞行'):

 

2. 创建数据成员并访问

数据成员是指在类中定义的变量,即属性,根据定义位置,又可以分为类属性和实例属性。

  • 类属性

类属性是指定义在类中,并且在函数体外的属性。类属性可以在类的所有实例之间共享值,也就是在所有实例化的对象中共用。

说明:类属性可以通过类名称或者实例名访问。

……

  • 实例属性

实例出行是指定义在类的方法中的属性,只作用于当前实例中。

7.2.5 访问限制

在类的内部可以定义属性和方法,而在类的外部则可以直接调用属性或方法来操作数据,从而隐藏了类内部的复杂逻辑。但是Python 并没有对属性和方法名前面添加单下划线(_foo)、双下划线(__foo)或首尾加双下划线(__foo__),从而限制访问权限。其中,单下划线、双下划线、首尾双下划线的作用如下:

(1)首尾双下划线表示定义特殊方法,一般是系统定义名字,如__init__()。

(2)以单下划线开头的表示protected(保护)类型的成员,只允许本身和子类进行访问,但不能使用“from module imort*”语句导入。

……

(3)双下划线表示private(私有)类型成员,只允许定义该方法的类本身进行访问,而且也不能通过类的实例进行访问,但是可以通过“类的实例名._类名__xxx”方式访问。

7.3 属性(property)

本节介绍的属性与7.2.4小节介绍的类属性和实例属性不同。7.2.4小节介绍的属性将返回存储的值,而本节介绍的属性则是一种特殊的属性,访问它时将计算它的值。另外,该属性还可以为属性添加安全保护机制。

7.3.1 创建用于计算的属性

在Python中,可以通过@property(装饰器)将一个方法转换为属性,从而实现用于计算的属性。将方法转换为属性后,可以直接通过方法名来访问方法,而不需要再添加一对小括号“o”,这样可以让代码更加简洁。

通过@property创建用于计算的属性的语法格式如下:

@property
def methodname(self):
    block

 

参数说明:

  • methodname:用于指定方法名,一般使用小写字母开头。该名称最后将作为创建的属性名。
  • self:必要参数,表示类的实例。
  • block:方法体,实现的具体功能。在方法体中,通常以return语句结束,用于返回计算结果。

……

注意:通过@property转换后的属性不能重复赋值,如果对其重新赋值,将抛出异常信息。

7.3.2 为属性添加安全保护机制

在Python中,默认情况下,创建的类属性或者实例是可以在类 体外进行修改的,如果想要限制其不能在类体外修改,可以将其设置为私有的,但设置为私有后,在类体外也不能获取它的值。如果想要创建一个可以读取但不能修改的属性,那么可以使用@property实现只读属性。

……

通过属性不仅可以将属性设置为只读属性,而且可以为属性设置拦截器,即允许对属性进行修改,但修改时需要遵守一定的约束。

7.4 继承

在编写类时,并不是每次都要从空白开始。当要编写的类和另一个已经存在的类之间存在一定的继承关系时,就可以通过继承来达到代码重用的目的,提高开发效率。下面介绍如果在Python中实现继承。

7.4.1 继承的基本语法

继承是面向对象编程最重要的特性之一,它源于人们认识客观世界的过程,是自然界普遍存在的一种现象。例如,我们每一个人都从祖辈和父母那里继承了一些体貌特征,但是每个人却又不同与父母,因为每个人都存在自己的一些特性,这些特性是独有的,在父母身上并没有体现。在程序设计中实现继承,表示这个类拥有它继承的类的素有公有成员或者受保护成员。在面向对象编程中,被继承的类称为父类或基类,新的类称为子类或派生类。

通过继承不仅可以实现代码的重用,还可以通过继承来顺类与类之间的关系。在Python中,可以在类定义语句中,类名右侧使用一对小括号将要继承的类名称括起来,从而实现类的继承。

……

参数说明:

  • ClassName:用于指定类名。
  • baseclasslist:用于指定要继承的基类,可以有多个,类名之间用逗号“,”分隔。如果不指定,将使所有Python对象的根类object。
  • '''类的帮助信息''':用于指定类的文档字符串,定义该字符串后,在创建类的对象时,输入类名和左侧的括号“(”后,将显示信息。
  • statement:类体,主要由类变量(或类成员)、方法和属性等定义语句组成。如果在定义类时,没想好类的具体功能,也可以在类体中直接使用pass语句代替。

……

7.4.2 方法重写

基类的成员都会被派生类继承,当基类中的某个方法不完全使用与派生类时,就需要在派生类中重写父类的这个方法,这个和Java语言中的方法重写是一样的。

7.4.3 派生类中调用基类的__init__()方法

在派生类中定义__init__()方法时,不会自动调用基类的__init__()方法。例如,定义一个Fruit类,在__init__()方法中创建类属性color,然后在Fruit类中定义一个harvest()方法。